text:
function unwrapping_main(args)
    version = "3.3.3"

    settings = getargs(args, version)
    keyargs = Dict()

    writedir = settings["output"]
    filename = "unwrapped"
    if occursin(r"\.nii$", writedir)
        filename = basename(writedir)
        writedir = dirname(writedir)
    end

    if settings["weights"] == "romeo"
        if isnothing(settings["magnitude"])
            settings["weights"] = "romeo4"
        else
            settings["weights"] = "romeo3"
        end
    end

    if settings["mask-unwrapped"] && settings["mask"][1] == "nomask"
        settings["mask"][1] = "robustmask"
    end

    if settings["mask"][1] == "robustmask" && isnothing(settings["magnitude"])
        settings["mask"][1] = "nomask"
        @warn "robustmask was chosen but no magnitude is available. No mask is used!"
    end

    phase = readphase(settings["phase"], mmap=!settings["no-mmap"], rescale=!settings["no-rescale"])
    hdr = header(phase)
    neco = size(phase, 4)

    # activate phase-offset-correction as default (monopolar)
    multi_channel = size(phase, 5) > 1
    if (settings["compute-B0"] || multi_channel || settings["phase-offset-correction"] == "on") && settings["phase-offset-correction"] != "bipolar"
        settings["phase-offset-correction"] = "monopolar"
        settings["verbose"] && println("Phase offset correction with MCPC3D-S set to monopolar")
    end
    if neco == 1
        settings["phase-offset-correction"] = "off"
        settings["verbose"] && println("Phase offset correction with MCPC3D-S turned off (only one echo)")
    end

    mkpath(writedir)
    saveconfiguration(writedir, settings, args, version)

    ## Perform phase offset correction MCPC3D-S
    if settings["phase-offset-correction"] in ["monopolar", "bipolar"]
        polarity = settings["phase-offset-correction"]
        bipolar_correction = polarity == "bipolar"

        TEs = getTEs(settings, neco, :)
        if neco != length(TEs) error("Phase offset determination requires all echo times!") end
        if TEs[1] == TEs[2] error("The echo times need to be different for MCPC3D-S phase offset correction!") end

        settings["verbose"] && println("Perform phase offset correction with MCPC3D-S ($polarity)")
        settings["verbose"] && multi_channel && println("Perform coil combination with MCPC3D-S ($polarity)")

        po = zeros(eltype(phase), (size(phase)[1:3]...,size(phase,5)))
        mag = if !isnothing(settings["magnitude"]) readmag(settings["magnitude"], mmap=!settings["no-mmap"]) else ones(size(phase)) end # TODO trues instead ones?
        sigma_mm = get_phase_offset_smoothing_sigma(settings)
        sigma_vox = sigma_mm ./ header(phase).pixdim[2:4]
        phase, mcomb = mcpc3ds(phase, mag; TEs, po, bipolar_correction, σ=sigma_vox)

        if size(mag, 5) != 1
            keyargs[:mag] = mcomb
        end
        if multi_channel
            settings["verbose"] && println("Saving combined_phase, combined_mag and phase_offset")
            savenii(phase, "combined_phase", writedir, hdr)
            savenii(mcomb, "combined_mag", writedir, hdr)
        else
            settings["verbose"] && println("Saving corrected_phase and phase_offset")
            savenii(phase, "corrected_phase", writedir, hdr)
        end
        settings["write-phase-offsets"] && savenii(po, "phase_offset", writedir, hdr)
    end

    ## Echoes for unwrapping
    echoes = try
        getechoes(settings, neco)
    catch y
        if isa(y, BoundsError)
            error("echoes=$(join(settings["unwrap-echoes"], " ")): specified echo out of range! Number of echoes is $neco")
        else
            error("echoes=$(join(settings["unwrap-echoes"], " ")) wrongly formatted!")
        end
    end
    settings["verbose"] && println("Echoes are $echoes")

    keyargs[:TEs] = getTEs(settings, neco, echoes)
    settings["verbose"] && println("TEs are $(keyargs[:TEs])")

    ## Error messages
    if 1 < length(echoes) && length(echoes) != length(keyargs[:TEs])
        error("Number of chosen echoes is $(length(echoes)) ($neco in .nii data), but $(length(keyargs[:TEs])) TEs were specified!")
    end

    phase = phase[:,:,:,echoes]
    settings["verbose"] && println("Phase loaded!")

    if !isnothing(settings["magnitude"]) && !haskey(keyargs, :mag)
        magnii = readmag(settings["magnitude"], mmap=!settings["no-mmap"])
        if size(magnii)[1:3] != size(phase)[1:3] || size(magnii, 4) < maximum(echoes)
            error("size of magnitude and phase does not match!")
        end
        keyargs[:mag] = view(magnii,:,:,:,echoes) # view avoids copy
        settings["verbose"] && println("Magnitude loaded!")
    end

    keyargs[:correctglobal] = settings["correct-global"]
    keyargs[:weights] = parseweights(settings)
    keyargs[:maxseeds] = settings["max-seeds"]
    settings["verbose"] && keyargs[:maxseeds] != 1 && println("Maxseeds are $(keyargs[:maxseeds])")
    keyargs[:merge_regions] = settings["merge-regions"]
    settings["verbose"] && keyargs[:merge_regions] && println("Region merging is activated")
    keyargs[:correct_regions] = settings["correct-regions"]
    settings["verbose"] && keyargs[:correct_regions] && println("Region correcting is activated")
    keyargs[:wrap_addition] = settings["wrap-addition"]
    keyargs[:temporal_uncertain_unwrapping] = settings["temporal-uncertain-unwrapping"]
    keyargs[:individual] = settings["individual-unwrapping"]
    settings["verbose"] && println("individual unwrapping is $(keyargs[:individual])")
    keyargs[:template] = settings["template"]
    settings["verbose"] && !settings["individual-unwrapping"] && println("echo $(keyargs[:template]) used as template")

    ## set mask
    if isfile(settings["mask"][1])
        settings["verbose"] && println("Trying to read mask from file $(settings["mask"][1])")
        keyargs[:mask] = niread(settings["mask"][1]).raw .!= 0
        if size(keyargs[:mask]) != size(phase)[1:3]
            error("size of mask is $(size(keyargs[:mask])), but it should be $(size(phase)[1:3])!")
        end
    elseif settings["mask"][1] == "robustmask" && haskey(keyargs, :mag)
        settings["verbose"] && println("Calculate robustmask from magnitude, saved as mask.nii")
        mag = keyargs[:mag]
        template_echo = min(keyargs[:template], size(mag, 4))
        keyargs[:mask] = robustmask(mag[:,:,:,template_echo])
        savenii(keyargs[:mask], "mask", writedir, hdr)
    elseif settings["mask"][1] == "qualitymask"
        threshold = if length(settings["mask"]) > 1
            parse(Float32, settings["mask"][2])
        else
            0.1 # default threshold
        end
        qmap = romeovoxelquality(phase; keyargs...)
        keyargs[:mask] = mask_from_voxelquality(qmap, threshold)
        savenii(keyargs[:mask], "mask", writedir, hdr)
    elseif settings["mask"][1] != "nomask"
        opt = settings["mask"][1]
        error("masking option '$opt' is undefined" * ifelse(tryparse(Float32, opt) isa Float32, " (Maybe '-k qualitymask $opt' was meant?)", ""))
    end

    ## Perform unwrapping
    settings["verbose"] && println("perform unwrapping...")
    regions=zeros(UInt8, size(phase)[1:3]) # regions is an output
    unwrap!(phase; keyargs..., regions)
    settings["verbose"] && println("unwrapping finished!")

    if settings["max-seeds"] > 1
        settings["verbose"] && println("writing regions...")
        savenii(regions, "regions", writedir, hdr)
    end

    if settings["threshold"] != Inf
        max = settings["threshold"] * 2π
        phase[phase .> max] .= 0
        phase[phase .< -max] .= 0
    end

    if settings["mask-unwrapped"] && haskey(keyargs, :mask)
        phase .*= keyargs[:mask]
    end

    savenii(phase, filename, writedir, hdr)

    if settings["compute-B0"]
        if isempty(settings["echo-times"])
            error("echo times are required for B0 calculation! Unwrapping has been performed")
        end
        if !haskey(keyargs, :mag)
            if length(keyargs[:TEs]) > 1
                @warn "B0 frequency estimation without magnitude might result in poor handling of noise in later echoes!"
            end
            keyargs[:mag] = to_dim(exp.(-keyargs[:TEs]/20), 4) # T2*=20ms decay (low value to reduce noise problems in later echoes)
        end
        B0 = calculateB0_unwrapped(phase, keyargs[:mag], keyargs[:TEs])
        savenii(B0, "B0", writedir, hdr)
    end

    # no mask used for writing quality maps
    if settings["write-quality"]
        settings["verbose"] && println("Calculate and write quality map...")
        savenii(romeovoxelquality(phase; keyargs...), "quality", writedir, hdr)
    end
    if settings["write-quality-all"]
        for i in 1:6
            flags = falses(6)
            flags[i] = true
            settings["verbose"] && println("Calculate and write quality map $i...")
            voxelquality = romeovoxelquality(phase; keyargs..., weights=flags)
            if all(voxelquality[1:end-1,1:end-1,1:end-1] .== 1.0)
                settings["verbose"] && println("quality map $i skipped for the given inputs")
            else
                savenii(voxelquality, "quality_$i", writedir, hdr)
            end
        end
    end

    return 0
end


text:
function _webgen_subpage_css()
    @tags_noescape style

    style("
        #slider{
            width:100%;
            height:1450px;
            position:relative;
            overflow:hidden;
            float:left;
            padding:0;
        }

        .slide{
            position:absolute;
            width:100%;
            height:100%;
        }

        .slide-copy{
            position:absolute;
            bottom:0;
            left:0;
            padding:10px 20px 20px 20px;
            background:7f7f7f;
            background: rgba(0,0,0,0.5);
            width:100%;
            max-height:32%;
        }_webgen_subpage

        #_webgen_subpagext{
         _webgen_subpage:pointer;
            z-index:100;
            background:#666;
            height:50px;
            width:50px;
            display:inline-block;
            position:relative;
            top:210px;
            margin:0;
            padding:0;
            opacity:0.7;
            filter: alpha(opacity=70);
        }

        #next{
            float:right;
            right:-2px;
        }

        #prev{
            float:left;
            left:0;
        }

        .arrow-right {
            width: 0;
            height: 0;
            border-top: 15px solid transparent;
            border-bottom: 15px solid transparent;
            border-left: 15px solid #fff;
            position:relative;
            top:20%;
            right:-40%;
        }

        .arrow-left {
            width: 0;
            height: 0;
            border-top: 15px solid transparent;
            border-bottom: 15px solid transparent;
            border-right:15px solid #fff;
            position:relative;
            top:20%;
            left:30%;
        }
    "; class="init", :type=>"text/css")
end

function _webpage_subgen_slider_js()
    script("
    \$(document).ready(function() {
        // options
        var speed = 100; //transition speed - fade
        var autoswitch = false; //auto slider options
        var autoswitch_speed = 5000; //auto slider speed

        // add first initial active class
        \$(\".slide\")
          .first()
          .addClass(\"active\");

        // hide all slides
        \$(\".slide\").hide;

        // show only active class slide
        \$(\".active\").show();

        // Next Event Handler
        \$(\"#next\").on(\"click\", nextSlide); // call function nextSlide

        // Prev Event Handler
        \$(\"#prev\").on(\"click\", prevSlide); // call function prevSlide

        document.onkeydown = function(evt) {
            evt = evt || window.event;
            switch (evt.keyCode) {
                case 37:
                    prevSlide();
                    break;
                case 39:
                    nextSlide();
                    break;
            }
        };

        // Auto Slider Handler
        if (autoswitch == true) {
          setInterval(nextSlide, autoswitch_speed); // call function and value 4000
        }

        // Switch to next slide
        function nextSlide() {
          \$(\".active\")
            .removeClass(\"active\")
            .addClass(\"oldActive\");
          if (\$(\".oldActive\").is(\":last-child\")) {
            \$(\".slide\")
              .first()
              .addClass(\"active\");
          } else {
            \$(\".oldActive\")
              .next()
              .addClass(\"active\");
          }
          \$(\".oldActive\").removeClass(\"oldActive\");
          \$(\".slide\").fadeOut(speed);
          \$(\".active\").fadeIn(speed);
        }

        // Switch to prev slide
        function prevSlide() {
          \$(\".active\")
            .removeClass(\"active\")
            .addClass(\"oldActive\");
          if (\$(\".oldActive\").is(\":first-child\")) {
            \$(\".slide\")
              .last()
              .addClass(\"active\");
          } else {
            \$(\".oldActive\")
              .prev()
              .addClass(\"active\");
          }
          \$(\".oldActive\").removeClass(\"oldActive\");
          \$(\".slide\").fadeOut(speed);
          \$(\".active\").fadeIn(speed);
        }
      });
    ")
end

function _webgen_report_intro(mission::Mission, obs_row::DataFrames.DataFrameRow{DataFrames.DataFrame},
        report_page_dir::String; e_range=_mission_good_e_range(mission)
    )
    obsid = obs_row[:obsid]
    name  = obs_row[:name]
    abstract_text = obs_row[:abstract]

    log_reports = _log_query(mission, obs_row, "web"; surpress_warn=true)
    if ismissing(log_reports)
        report_df = DataFrame()
    else
        report_e_ranges = ["$(e_r[1]) to $(e_r[2]) keV" for e_r in keys(log_reports)]
        report_rel_path = [a(link, href=link) for link in [replace(path, report_page_dir=>"..") for path in values(log_reports)]]
        report_df = DataFrame(e_range=report_e_ranges, report=report_rel_path)
    end
    node_intro = div(
        h1("Observation $obsid - $name - $e_range keV"),
        h2("Available Energy Range Reports"),
        _webgen_table(report_df, ""; table_id="report_page"),
        h2("Abstract"),
        p(abstract_text),
        hr(),
        h4("Status"),
        _webgen_table(obs_row[[:public_date, :publicity, :time]], ""; table_id="report_page"),
        h4("Source Details"),
        _webgen_table(obs_row[[:name, :ra, :dec, :lii, :bii, :obs_type]], ""; table_id=""),
        h4("Observation Details"),
        _webgen_table(obs_row[[:time, :end_time, :exposure, :remarks]], ""; table_id=""),
        h4("Misc"),
        _webgen_table(obs_row[[:processing_status, :processing_date, :processing_version, :num_processed, :caldb_version]], "", table_id="")
    )
end

function _webgen_report_body(obs_row, img_df_overview)
    images = []
    for link in img_df_overview[:path]
        images = [images; img(src=string(repeat("../", 2), link))]
    end

    node_body = div(
        hr(),
        h2("Plots"),
        images
    )
end

function _webgen_report_body_groups(obs_row, img_df)
    groups = unique(img_df[:group])

    group_container = Array{Hyperscript.Node{Hyperscript.HTMLSVG},1}()
    for group in groups
        group_images = filter(x->x[:group]==group, img_df)

        node_group = div(class="slide",
            div(
                h4("group - $group"),
                [(img(src=string(repeat("../", 2), row[:path]))) for row in DataFrames.eachrow(group_images)]
            )
        )

        push!(group_container, node_group)
    end

    slider_node = div(
        h2("Per-Group Plots"),
        p("Use the left and right arrow keys to move between groups."),
        div(id="slider",
            div(id="next", ald="Next", title="Next",
                div(class="arrow-right")
            ),
            div(id="prev", alt="Prev", title="Prev",
                div(class="arrow-left")
            ),
            group_container
        )
    )

    return slider_node
end

function _webgen_subpage_footer()
    div(
        hr(),
        h4("Notes"),
        h5("Lightcurve"),
            p("Plot of events binned to 1-second intervals. Red/green vertical lines show the start/stop times of the GTIs."),
        h5("Periodogram"),
            p("Periodograms created with the LombScargle.jl `periodogram` function, using the `:standard` normalisation, which is based on Zechmeister, M., Kürster, M. 2009, A&A, 496, 577."),
        h5("Power Spectra"),
            p("Leahy-normalised power spectra, amplitudes -2, then multipled by the frequency. Both x and y axis are log10 scale."),
        h5("Spectrogram"),
            p("Spectrograms are made by plotting each individual power spectra as a row on the heatmap. Normalisation is the same as for the power spectra."),
            p("Note that when looking at the spectrogram the gaps in the lightcurve are not displayed, so trends shown in the spectrogram may not represent reality. Currently plotting function limitations mean that the x-axis ticks are not accurate for the spectorgram, so they have been disabled.
            The spectrogram should only be used as an indication of QPOs moving over time, further analysis should be performed using external software."),
            p("The orange horizontal lines denote the boundry between different groups. The zone under a line belongs to the group on the line's y-axis tick."),
        h5("Pulsation Plots"),
            p("Only powers above 30 are plotted. Instead of an average, as for the FFTs above, each individual power spectra is used."),
            p("Candle lines go up to the power, a scatter plot with dots is overlayed on top as well to help show the density of points."),
            p("Pulsation plots are used to find intermittent/weak pulsations which may be hidden by the averaging done for the main power spectra plots"),
        # h5("Pulsation Spectrogram"),
        #     p("Spectrograms with a `:freq_binary` rebin are used to search for pulsations."),
        #     p("The rebin has two numbers: the first signifies the size of the frequency bins (by default 10 Hz), and the second is an array of threshold values (default 10, 25, 50)."),
        #     p("If any values of the power spectra in the frequency bins is above one of the thresholds, the point is set to the threshold value."),
        #     p("This makes it easy to spot any high-frequency, intermittent pulsations, as they will show up as a pattern of bright points/bands."),
        #     p("A good example of this is the nicer observation `1013010126` (PSR_B0531+21), which shows clear, bright, banding patterns from ~50 to ~500 Hz."),
        h5("Groups"),
            p("\"Groups\" are GTIs seperated by less than 128 seconds, which have been grouped together. They are used to select smaller chunks of the lightcurve, which are then passed through periodogram and power spectra functions. Left and right arrow keys can be used to move between groups."),
    )
end

function _webgen_subpage(mission::Mission, obs_row::DataFrames.DataFrameRow{DataFrames.DataFrame}; e_range=_mission_good_e_range(mission))
    path_web = abspath(_obs_path_local(mission, obs_row; kind=:web), "JAXTAM", "web")
    mkpath(path_web)

    log_images  = _log_query(mission, obs_row, "images", e_range)

    if ismissing(log_images)
        throw(JAXTAMError("No images in log, no web page to generate", :webgen_subpage))
    end

    img_details_overview = filter(x->ismissing(x[:group]), log_images)
    img_details_overview = sort(img_details_overview, (:group, :kind_order))

    img_details_groups   = filter(x->!ismissing(x[:group]), log_images)
    img_details_groups   = sort(img_details_groups, (:group, :kind_order))

    html_out = html(
        _webgen_head(;title_in="$(_mission_name(mission)) - $e_range keV - $(obs_row[:name]) - $(obs_row[:obsid]) - Reports"),
        _webgen_subpage_css(),
        _webpage_subgen_slider_js(),
        body(
            div(class="se-pre-con"),
            div(class="container",
                _webgen_report_intro(mission, obs_row, path_web; e_range=e_range),
                _webgen_report_body(obs_row, img_details_overview),
                _webgen_report_body_groups(obs_row, img_details_groups),
                _webgen_subpage_footer()
            )
        )
    )

    path_report = joinpath(path_web, "$e_range", "report.html")
    mkpath(dirname(path_report))

    write(path_report, string(Pretty(html_out)))
    _log_add(mission, obs_row,
        Dict("web" =>
            Dict(e_range => path_report)
        )
    )
    return path_report
end

function webgen_subpage(mission::Mission, obsid::String; e_range=_mission_good_e_range(mission))
    obs_row = master_query(mission, :obsid, obsid)

    return _webgen_subpage(mission, obs_row; e_range=e_range)
end

ext: jl


text:
""" `FillArrays` module to lazily represent matrices with a single value """
module FillArrays

using LinearAlgebra, SparseArrays
import Base: size, getindex, setindex!, IndexStyle, checkbounds, convert,
    +, -, *, /, \, diff, sum, cumsum, maximum, minimum, sort, sort!,
    any, all, axes, isone, iterate, unique, allunique, permutedims, inv,
    copy, vec, setindex!, count, ==, reshape, _throw_dmrs, map, zero,
    show, view, in

import LinearAlgebra: rank, svdvals!, tril, triu, tril!, triu!, diag, transpose, adjoint, fill!,
    norm2, norm1, normInf, normMinusInf, normp, lmul!, rmul!, diagzero, AbstractTriangular, AdjointAbsVec

import Base.Broadcast: broadcasted, DefaultArrayStyle, broadcast_shape

export Zeros, Ones, Fill, Eye, Trues, Falses

if VERSION < v"1.6-"
    oneto(n) = Base.OneTo(n)
else
    import Base: oneto
end

"""
    AbstractFill{T, N, Axes} <: AbstractArray{T, N}

Supertype for lazy array types whose entries are all equal to constant.
"""
abstract type AbstractFill{T, N, Axes} <: AbstractArray{T, N} end

==(a::AbstractFill, b::AbstractFill) = axes(a) == axes(b) && getindex_value(a) == getindex_value(b)

@inline function getindex(F::AbstractFill, k::Integer)
    @boundscheck checkbounds(F, k)
    getindex_value(F)
end

@inline function getindex(F::AbstractFill{T, N}, kj::Vararg{<:Integer, N}) where {T, N}
    @boundscheck checkbounds(F, kj...)
    getindex_value(F)
end

@inline function setindex!(F::AbstractFill, v, k::Integer)
    @boundscheck checkbounds(F, k)
    v == getindex_value(F) || throw(ArgumentError("Cannot setindex! to $v for an AbstractFill with value $(getindex_value(F))."))
    F
end

@inline function setindex!(F::AbstractFill{T, N}, v, kj::Vararg{<:Integer, N}) where {T, N}
    @boundscheck checkbounds(F, kj...)
    v == getindex_value(F) || throw(ArgumentError("Cannot setindex! to $v for an AbstractFill with value $(getindex_value(F))."))
    F
end

@inline function fill!(F::AbstractFill, v)
    v == getindex_value(F) || throw(ArgumentError("Cannot fill! with $v an AbstractFill with value $(getindex_value(F))."))
    F
end

rank(F::AbstractFill) = iszero(getindex_value(F)) ? 0 : 1
IndexStyle(::Type{<:AbstractFill{<:Any,N,<:NTuple{N,Base.OneTo{Int}}}}) where N = IndexLinear()

"""
    Fill{T, N, Axes}
    where `Axes <: Tuple{Vararg{AbstractUnitRange,N}}`

A lazy representation of an array of dimension `N`
whose entries are all equal to a constant of type `T`,
with axes of type `Axes`.
Typically created by `Fill` or `Zeros` or `Ones`

# Examples

```jldoctest
julia> Fill(7, (2,3))
2×3 Fill{Int64,2,Tuple{Base.OneTo{Int64},Base.OneTo{Int64}}}:
 7  7  7
 7  7  7

julia> Fill{Float64, 1, Tuple{UnitRange{Int64}}}(7., (1:2,))
2-element Fill{Float64,1,Tuple{UnitRange{Int64}}} with indices 1:2:
 7.0
 7.0
```
"""
struct Fill{T, N, Axes} <: AbstractFill{T, N, Axes}
    value::T
    axes::Axes

    Fill{T,N,Axes}(x::T, sz::Axes) where Axes<:Tuple{Vararg{AbstractUnitRange,N}} where {T, N} =
        new{T,N,Axes}(x,sz)
    Fill{T,0,Tuple{}}(x::T, sz::Tuple{}) where T = new{T,0,Tuple{}}(x,sz)
end

Fill{T,N,Axes}(x, sz::Axes) where Axes<:Tuple{Vararg{AbstractUnitRange,N}} where {T, N} =
    Fill{T,N,Axes}(convert(T, x)::T, sz)

Fill{T,0}(x::T, ::Tuple{}) where T = Fill{T,0,Tuple{}}(x, ()) # ambiguity fix

@inline Fill{T, N}(x::T, sz::Axes) where Axes<:Tuple{Vararg{AbstractUnitRange,N}} where {T, N} =
    Fill{T,N,Axes}(x, sz)
@inline Fill{T, N}(x, sz::Axes) where Axes<:Tuple{Vararg{AbstractUnitRange,N}} where {T, N} =
    Fill{T,N}(convert(T, x)::T, sz)

@inline Fill{T, N}(x, sz::SZ) where SZ<:Tuple{Vararg{Integer,N}} where {T, N} =
    Fill{T,N}(x, oneto.(sz))
@inline Fill{T, N}(x, sz::Vararg{Integer, N}) where {T, N} = Fill{T,N}(convert(T, x)::T, sz)

@inline Fill{T}(x, sz::Vararg{<:Integer,N}) where {T, N} = Fill{T, N}(x, sz)
@inline Fill{T}(x, sz::Tuple{Vararg{<:Any,N}}) where {T, N} = Fill{T, N}(x, sz)
""" `Fill(x, dims...)` construct lazy version of `fill(x, dims...)` """
@inline Fill(x::T, sz::Vararg{<:Integer,N}) where {T, N}  = Fill{T, N}(x, sz)
""" `Fill(x, dims)` construct lazy version of `fill(x, dims)` """
@inline Fill(x::T, sz::Tuple{Vararg{<:Any,N}}) where {T, N}  = Fill{T, N}(x, sz)

# We restrict to  when T is specified to avoid ambiguity with a Fill of a Fill
@inline Fill{T}(F::Fill{T}) where T = F
@inline Fill{T,N}(F::Fill{T,N}) where {T,N} = F
@inline Fill{T,N,Axes}(F::Fill{T,N,Axes}) where {T,N,Axes} = F

@inline axes(F::Fill) = F.axes
@inline size(F::Fill) = length.(F.axes)

@inline getindex_value(F::Fill) = F.value

AbstractArray{T}(F::Fill{T}) where T = F
AbstractArray{T,N}(F::Fill{T,N}) where {T,N} = F
AbstractArray{T}(F::Fill{V,N}) where {T,V,N} = Fill{T}(convert(T, F.value)::T, F.axes)
AbstractArray{T,N}(F::Fill{V,N}) where {T,V,N} = Fill{T}(convert(T, F.value)::T, F.axes)

convert(::Type{AbstractArray{T}}, F::Fill{T}) where T = F
convert(::Type{AbstractArray{T,N}}, F::Fill{T,N}) where {T,N} = F
convert(::Type{AbstractArray{T}}, F::Fill) where {T} = AbstractArray{T}(F)
convert(::Type{AbstractArray{T,N}}, F::Fill) where {T,N} = AbstractArray{T,N}(F)
convert(::Type{AbstractFill}, F::AbstractFill) = F
convert(::Type{AbstractFill{T}}, F::AbstractFill) where T = convert(AbstractArray{T}, F)
convert(::Type{AbstractFill{T,N}}, F::AbstractFill) where {T,N} = convert(AbstractArray{T,N}, F)

copy(F::Fill) = Fill(F.value, F.axes)

""" Throws an error if `arr` does not contain one and only one unique value. """
function unique_value(arr::AbstractArray)
    if isempty(arr) error("Cannot convert empty array to Fill") end
    val = first(arr)
    for x in arr
        if x !== val
            error("Input array contains both $x and $val. Cannot convert to Fill")
        end
    end
    return val
end
unique_value(f::AbstractFill) = getindex_value(f)
convert(::Type{Fill}, arr::AbstractArray{T}) where T = Fill{T}(unique_value(arr), axes(arr))
convert(::Type{Fill{T}}, arr::AbstractArray) where T = Fill{T}(unique_value(arr), axes(arr))
convert(::Type{Fill{T,N}}, arr::AbstractArray{<:Any,N}) where {T,N} = Fill{T,N}(unique_value(arr), axes(arr))
convert(::Type{Fill{T,N,Axes}}, arr::AbstractArray{<:Any,N}) where {T,N,Axes} = Fill{T,N,Axes}(unique_value(arr), axes(arr))
# ambiguity fix
convert(::Type{Fill}, arr::Fill{T}) where T = Fill{T}(unique_value(arr), axes(arr))
convert(::Type{T}, F::T) where T<:Fill = F

getindex(F::Fill{<:Any,0}) = getindex_value(F)

function Base._unsafe_getindex(::IndexStyle, F::AbstractFill, I::Vararg{Union{Real, AbstractArray}, N}) where N
    shape = Base.index_shape(I...)
    fillsimilar(F, shape)
end

function getindex(A::AbstractFill, kr::AbstractVector{Bool})
   length(A) == length(kr) || throw(DimensionMismatch())
   fillsimilar(A, count(kr))
end
function getindex(A::AbstractFill, kr::AbstractArray{Bool})
   size(A) == size(kr) || throw(DimensionMismatch())
   fillsimilar(A, count(kr))
end

sort(a::AbstractFill; kwds...) = a
sort!(a::AbstractFill; kwds...) = a
svdvals!(a::AbstractFill{<:Any,2}) = [getindex_value(a)*sqrt(prod(size(a))); Zeros(min(size(a)...)-1)]

+(a::AbstractFill) = a
-(a::AbstractFill) = Fill(-getindex_value(a), size(a))

# Fill +/- Fill
function +(a::AbstractFill{T, N}, b::AbstractFill{V, N}) where {T, V, N}
    axes(a) ≠ axes(b) && throw(DimensionMismatch("dimensions must match."))
    return Fill(getindex_value(a) + getindex_value(b), axes(a))
end
-(a::AbstractFill, b::AbstractFill) = a + (-b)

function +(a::Fill{T, 1}, b::AbstractRange) where {T}
    size(a) ≠ size(b) && throw(DimensionMismatch("dimensions must match."))
    Tout = promote_type(T, eltype(b))
    return (a.value + first(b)):convert(Tout, step(b)):(a.value + last(b))
end
function +(a::Fill{T, 1}, b::UnitRange) where {T}
    size(a) ≠ size(b) && throw(DimensionMismatch("dimensions must match."))
    Tout = promote_type(T, eltype(b))
    return (a.value + first(b)):(a.value + last(b))
end
+(a::AbstractRange, b::AbstractFill) = b + a

-(a::AbstractFill, b::AbstractRange) = a + (-b)
-(a::AbstractRange, b::AbstractFill) = a + (-b)

function fill_reshape(parent, dims::Integer...)
    n = length(parent)
    prod(dims) == n || _throw_dmrs(n, "size", dims)
    fillsimilar(parent, dims...)
end

reshape(parent::AbstractFill, dims::Integer...) = reshape(parent, dims)
reshape(parent::AbstractFill, dims::Union{Int,Colon}...) = reshape(parent, dims)
reshape(parent::AbstractFill, dims::Union{Integer,Colon}...) = reshape(parent, dims)

reshape(parent::AbstractFill, dims::Tuple{Vararg{Union{Integer,Colon}}}) =
    fill_reshape(parent, Base._reshape_uncolon(parent, dims)...)
reshape(parent::AbstractFill, dims::Tuple{Vararg{Union{Int,Colon}}}) =
    fill_reshape(parent, Base._reshape_uncolon(parent, dims)...)
reshape(parent::AbstractFill, shp::Tuple{Union{Integer,Base.OneTo}, Vararg{Union{Integer,Base.OneTo}}}) =
    reshape(parent, Base.to_shape(shp))
reshape(parent::AbstractFill, dims::Dims)        = Base._reshape(parent, dims)
reshape(parent::AbstractFill, dims::Tuple{Integer, Vararg{Integer}})        = Base._reshape(parent, dims)
Base._reshape(parent::AbstractFill, dims::Dims) = fill_reshape(parent, dims...)
Base._reshape(parent::AbstractFill, dims::Tuple{Integer,Vararg{Integer}}) = fill_reshape(parent, dims...)
# Resolves ambiguity error with `_reshape(v::AbstractArray{T, 1}, dims::Tuple{Int})`
Base._reshape(parent::AbstractFill{T, 1, Axes}, dims::Tuple{Int}) where {T, Axes} = fill_reshape(parent, dims...)

for (Typ, funcs, func) in ((:Zeros, :zeros, :zero), (:Ones, :ones, :one))
    @eval begin
        """ `$($Typ){T, N, Axes} <: AbstractFill{T, N, Axes}` (lazy `$($funcs)` with axes)"""
        struct $Typ{T, N, Axes} <: AbstractFill{T, N, Axes}
            axes::Axes
            @inline $Typ{T,N,Axes}(sz::Axes) where Axes<:Tuple{Vararg{AbstractUnitRange,N}} where {T, N} =
                new{T,N,Axes}(sz)
            @inline $Typ{T,N}(sz::Axes) where Axes<:Tuple{Vararg{AbstractUnitRange,N}} where {T, N} =
                new{T,N,Axes}(sz)
            @inline $Typ{T,0,Tuple{}}(sz::Tuple{}) where T = new{T,0,Tuple{}}(sz)
        end

        @inline $Typ{T, 0}(sz::Tuple{}) where {T} = $Typ{T,0,Tuple{}}(sz)
        @inline $Typ{T, N}(sz::Tuple{Vararg{<:Integer, N}}) where {T, N} = $Typ{T,N}(oneto.(sz))
        @inline $Typ{T, N}(sz::Vararg{<:Integer, N}) where {T, N} = $Typ{T,N}(sz)
        """ `$($Typ){T}(dims...)` construct lazy version of `$($funcs)(dims...)`"""
        @inline $Typ{T}(sz::Vararg{Integer,N}) where {T, N} = $Typ{T, N}(sz)
        @inline $Typ{T}(sz::SZ) where SZ<:Tuple{Vararg{Any,N}} where {T, N} = $Typ{T, N}(sz)
        @inline $Typ(sz::Vararg{Any,N}) where N = $Typ{Float64,N}(sz)
        @inline $Typ(sz::SZ) where SZ<:Tuple{Vararg{Any,N}} where N = $Typ{Float64,N}(sz)
        @inline $Typ{T}(n::Integer) where T = $Typ{T,1}(n)
        @inline $Typ(n::Integer) = $Typ{Float64,1}(n)

        @inline $Typ{T,N,Axes}(A::AbstractArray{V,N}) where{T,V,N,Axes} = $Typ{T,N,Axes}(axes(A))
        @inline $Typ{T,N}(A::AbstractArray{V,N}) where{T,V,N} = $Typ{T,N}(size(A))
        @inline $Typ{T}(A::AbstractArray) where{T} = $Typ{T}(size(A))
        @inline $Typ(A::AbstractArray) = $Typ{eltype(A)}(A)

        @inline axes(Z::$Typ) = Z.axes
        @inline size(Z::$Typ) = length.(Z.axes)
        @inline getindex_value(Z::$Typ{T}) where T = $func(T)

        AbstractArray{T}(F::$Typ{T}) where T = F
        AbstractArray{T,N}(F::$Typ{T,N}) where {T,N} = F
        AbstractArray{T}(F::$Typ) where T = $Typ{T}(F.axes)
        AbstractArray{T,N}(F::$Typ{V,N}) where {T,V,N} = $Typ{T}(F.axes)
        convert(::Type{AbstractArray{T}}, F::$Typ{T}) where T = AbstractArray{T}(F)
        convert(::Type{AbstractArray{T,N}}, F::$Typ{T,N}) where {T,N} = AbstractArray{T,N}(F)
        convert(::Type{AbstractArray{T}}, F::$Typ) where T = AbstractArray{T}(F)
        convert(::Type{AbstractArray{T,N}}, F::$Typ) where {T,N} = AbstractArray{T,N}(F)

        copy(F::$Typ) = F

        getindex(F::$Typ{T,0}) where T = getindex_value(F)
    end
end

"""
    fillsimilar(a::AbstractFill, axes)

creates a fill object that has the same fill value as `a` but
with the specified axes.
For example, if `a isa Zeros` then so is the returned object.
"""
fillsimilar(a::Ones{T}, axes...) where T = Ones{T}(axes...)
fillsimilar(a::Zeros{T}, axes...) where T = Zeros{T}(axes...)
fillsimilar(a::AbstractFill, axes...) = Fill(getindex_value(a), axes...)

rank(F::Zeros) = 0
rank(F::Ones) = 1

struct RectDiagonal{T,V<:AbstractVector{T},Axes<:Tuple{Vararg{AbstractUnitRange,2}}} <: AbstractMatrix{T}
    diag::V
    axes::Axes

    @inline function RectDiagonal{T,V}(A::V, axes::Axes) where {T,V<:AbstractVector{T},Axes<:Tuple{Vararg{AbstractUnitRange,2}}}
        @assert !Base.has_offset_axes(A)
        @assert any(length(ax) == length(A) for ax in axes)
        rd = new{T,V,Axes}(A, axes)
        @assert !Base.has_offset_axes(rd)
        return rd
    end
end

@inline RectDiagonal{T,V}(A::V, sz::Tuple{Vararg{Integer, 2}}) where {T,V} = RectDiagonal{T,V}(A, oneto.(sz))
@inline RectDiagonal{T,V}(A::V, axes::Vararg{Any, 2}) where {T,V} = RectDiagonal{T,V}(A, axes)
@inline RectDiagonal{T,V}(A::V, sz::Vararg{Integer, 2}) where {T,V} = RectDiagonal{T,V}(A, sz)
@inline RectDiagonal{T,V}(A::V) where {T,V} = RectDiagonal{T,V}(A, (axes(A, 1), axes(A, 1)))
@inline RectDiagonal{T}(A::V, args...) where {T,V} = RectDiagonal{T,V}(A, args...)
@inline RectDiagonal(A::V, args...) where {V} = RectDiagonal{eltype(V),V}(A, args...)

# patch missing overload from Base
axes(rd::Diagonal{<:Any,<:AbstractFill}) = (axes(rd.diag,1),axes(rd.diag,1))
axes(T::AbstractTriangular{<:Any,<:AbstractFill}) = axes(parent(T))

axes(rd::RectDiagonal) = rd.axes
size(rd::RectDiagonal) = length.(rd.axes)

@inline function getindex(rd::RectDiagonal{T}, i::Integer, j::Integer) where T
    @boundscheck checkbounds(rd, i, j)
    if i == j
        @inbounds r = rd.diag[i]
    else
        r = zero(T)
    end
    return r
end

function setindex!(rd::RectDiagonal, v, i::Integer, j::Integer)
    @boundscheck checkbounds(rd, i, j)
    if i == j
        @inbounds rd.diag[i] = v
    elseif !iszero(v)
        throw(ArgumentError("cannot set off-diagonal entry ($i, $j) to a nonzero value ($v)"))
    end
    return v
end

diag(rd::RectDiagonal) = rd.diag

for f in (:triu, :triu!, :tril, :tril!)
    @eval ($f)(M::RectDiagonal) = M
end

Base.replace_in_print_matrix(A::RectDiagonal, i::Integer, j::Integer, s::AbstractString) =
    i == j ? s : Base.replace_with_centered_mark(s)

const RectOrDiagonal{T,V,Axes} = Union{RectDiagonal{T,V,Axes}, Diagonal{T,V}}
const SquareEye{T,Axes} = Diagonal{T,Ones{T,1,Tuple{Axes}}}
const Eye{T,Axes} = RectOrDiagonal{T,Ones{T,1,Tuple{Axes}}}

@inline SquareEye{T}(n::Integer) where T = Diagonal(Ones{T}(n))
@inline SquareEye(n::Integer) = Diagonal(Ones(n))
@inline SquareEye{T}(ax::Tuple{AbstractUnitRange{Int}}) where T = Diagonal(Ones{T}(ax))
@inline SquareEye(ax::Tuple{AbstractUnitRange{Int}}) = Diagonal(Ones(ax))

@inline Eye{T}(n::Integer) where T = SquareEye{T}(n)
@inline Eye(n::Integer) = SquareEye(n)
@inline Eye{T}(ax::Tuple{AbstractUnitRange{Int}}) where T = SquareEye{T}(ax)
@inline Eye(ax::Tuple{AbstractUnitRange{Int}}) = SquareEye(ax)

# function iterate(iter::Eye, istate = (1, 1))
#     (i::Int, j::Int) = istate
#     m = size(iter, 1)
#     return i > m ? nothing :
#         ((@inbounds getindex(iter, i, j)),
#          j == m ? (i + 1, 1) : (i, j + 1))
# end

isone(::SquareEye) = true

for f in (:permutedims, :inv, :triu, :triu!, :tril, :tril!)
    @eval ($f)(IM::SquareEye) = IM
end

Eye(n::Integer, m::Integer) = RectDiagonal(Ones(min(n,m)), n, m)
Eye{T}(n::Integer, m::Integer) where T = RectDiagonal{T}(Ones{T}(min(n,m)), n, m)
function Eye{T}((a,b)::NTuple{2,AbstractUnitRange{Int}}) where T
    ab = length(a) ≤ length(b) ? a : b
    RectDiagonal{T}(Ones{T}((ab,)), (a,b))
end
function Eye((a,b)::NTuple{2,AbstractUnitRange{Int}})
    ab = length(a) ≤ length(b) ? a : b
    RectDiagonal(Ones((ab,)), (a,b))
end

@deprecate Eye{T}(sz::Tuple{Vararg{Integer,2}}) where T Eye{T}(sz...)
@deprecate Eye(sz::Tuple{Vararg{Integer,2}}) Eye{Float64}(sz...)

@inline Eye{T}(A::AbstractMatrix) where T = Eye{T}(size(A)...)
@inline Eye(A::AbstractMatrix) = Eye{eltype(A)}(size(A)...)

#########
#  Special matrix types
#########

## Array
convert(::Type{Array}, F::AbstractFill) = fill(getindex_value(F), size(F))
convert(::Type{Array{T}}, F::AbstractFill) where T = fill(convert(T, getindex_value(F)), size(F))
convert(::Type{Array{T,N}}, F::AbstractFill{V,N}) where {T,V,N} = fill(convert(T, getindex_value(F)), size(F))

# These are in case `zeros` or `ones` are ever faster than `fill`
for (Typ, funcs, func) in ((:Zeros, :zeros, :zero), (:Ones, :ones, :one))
    @eval begin
        convert(::Type{Array}, F::$Typ{T}) where T = $funcs(T, size(F))
        convert(::Type{Array{T}}, F::$Typ{T}) where T = $funcs(T, size(F))
        convert(::Type{Array{T,N}}, F::$Typ{V,N}) where {T,V,N} = $funcs(T,size(F))
    end
end

function convert(::Type{Diagonal}, Z::Zeros{T,2}) where T
    n,m = size(Z)
    n ≠ m && throw(BoundsError(Z))
    Diagonal(zeros(T, n))
end

function convert(::Type{Diagonal{T}}, Z::Zeros{V,2}) where {T,V}
    n,m = size(Z)
    n ≠ m && throw(BoundsError(Z))
    Diagonal(zeros(T, n))
end

## Sparse arrays

convert(::Type{SparseVector}, Z::Zeros{T,1}) where T = spzeros(T, length(Z))
convert(::Type{SparseVector{Tv}}, Z::Zeros{T,1}) where {T,Tv} = spzeros(Tv, length(Z))
convert(::Type{SparseVector{Tv,Ti}}, Z::Zeros{T,1}) where {T,Tv,Ti} = spzeros(Tv, Ti, length(Z))

convert(::Type{AbstractSparseVector}, Z::Zeros{T,1}) where T = spzeros(T, length(Z))
convert(::Type{AbstractSparseVector{Tv}}, Z::Zeros{T,1}) where {Tv,T}= spzeros(Tv, length(Z))

convert(::Type{SparseMatrixCSC}, Z::Zeros{T,2}) where T = spzeros(T, size(Z)...)
convert(::Type{SparseMatrixCSC{Tv}}, Z::Zeros{T,2}) where {T,Tv} = spzeros(Tv, size(Z)...)
convert(::Type{SparseMatrixCSC{Tv,Ti}}, Z::Zeros{T,2}) where {T,Tv,Ti} = spzeros(Tv, Ti, size(Z)...)
convert(::Type{SparseMatrixCSC{Tv,Ti}}, Z::Zeros{T,2,Axes}) where {Tv,Ti<:Integer,T,Axes} =
    spzeros(Tv, Ti, size(Z)...)

convert(::Type{AbstractSparseMatrix}, Z::Zeros{T,2}) where {T} = spzeros(T, size(Z)...)
convert(::Type{AbstractSparseMatrix{Tv}}, Z::Zeros{T,2}) where {T,Tv} = spzeros(Tv, size(Z)...)

convert(::Type{AbstractSparseArray}, Z::Zeros{T}) where T = spzeros(T, size(Z)...)
convert(::Type{AbstractSparseArray{Tv}}, Z::Zeros{T}) where {T,Tv} = spzeros(Tv, size(Z)...)
convert(::Type{AbstractSparseArray{Tv,Ti}}, Z::Zeros{T}) where {T,Tv,Ti} = spzeros(Tv, Ti, size(Z)...)
convert(::Type{AbstractSparseArray{Tv,Ti,N}}, Z::Zeros{T,N}) where {T,Tv,Ti,N} = spzeros(Tv, Ti, size(Z)...)

convert(::Type{SparseMatrixCSC}, Z::Eye{T}) where T = SparseMatrixCSC{T}(I, size(Z)...)
convert(::Type{SparseMatrixCSC{Tv}}, Z::Eye{T}) where {T,Tv} = SparseMatrixCSC{Tv}(I, size(Z)...)
# works around missing `speye`:
convert(::Type{SparseMatrixCSC{Tv,Ti}}, Z::Eye{T}) where {T,Tv,Ti<:Integer} =
    convert(SparseMatrixCSC{Tv,Ti}, SparseMatrixCSC{Tv}(I, size(Z)...))

convert(::Type{AbstractSparseMatrix}, Z::Eye{T}) where {T} = SparseMatrixCSC{T}(I, size(Z)...)
convert(::Type{AbstractSparseMatrix{Tv}}, Z::Eye{T}) where {T,Tv} = SparseMatrixCSC{Tv}(I, size(Z)...)

convert(::Type{AbstractSparseArray}, Z::Eye{T}) where T = SparseMatrixCSC{T}(I, size(Z)...)
convert(::Type{AbstractSparseArray{Tv}}, Z::Eye{T}) where {T,Tv} = SparseMatrixCSC{Tv}(I, size(Z)...)

convert(::Type{AbstractSparseArray{Tv,Ti}}, Z::Eye{T}) where {T,Tv,Ti} =
    convert(SparseMatrixCSC{Tv,Ti}, Z)
convert(::Type{AbstractSparseArray{Tv,Ti,2}}, Z::Eye{T}) where {T,Tv,Ti} =
    convert(SparseMatrixCSC{Tv,Ti}, Z)

#########
# maximum/minimum
#########

for op in (:maximum, :minimum)
    @eval $op(x::AbstractFill) = getindex_value(x)
end

#########
# Cumsum
#########

sum(x::AbstractFill) = getindex_value(x)*length(x)
sum(x::Zeros) = getindex_value(x)

# define `sum(::Callable, ::AbstractFill)` to avoid method ambiguity errors on Julia 1.0
sum(f, x::AbstractFill) = _sum(f, x)
sum(f::Base.Callable, x::AbstractFill) = _sum(f, x)
_sum(f, x::AbstractFill) = length(x) * f(getindex_value(x))

cumsum(x::AbstractFill{<:Any,1}) = range(getindex_value(x); step=getindex_value(x),
                                                    length=length(x))

cumsum(x::Zeros{<:Any,1}) = x
cumsum(x::Zeros{Bool,1}) = x
cumsum(x::Ones{II,1}) where II<:Integer = convert(AbstractVector{II}, oneto(length(x)))
cumsum(x::Ones{Bool,1}) = oneto(length(x))
cumsum(x::AbstractFill{Bool,1}) = cumsum(convert(AbstractFill{Int}, x))

#########
# Diff
#########

diff(x::AbstractFill{T,1}) where T = Zeros{T}(length(x)-1)

#########
# unique
#########

unique(x::AbstractFill{T}) where T = isempty(x) ? T[] : T[getindex_value(x)]
allunique(x::AbstractFill) = length(x) < 2

#########
# zero
#########

zero(r::Zeros{T,N}) where {T,N} = r
zero(r::Ones{T,N}) where {T,N} = Zeros{T,N}(r.axes)
zero(r::Fill{T,N}) where {T,N} = Zeros{T,N}(r.axes)

#########
# any/all/isone/iszero
#########

function isone(AF::AbstractFill{<:Any,2})
    isone(getindex_value(AF)) || return false
    (n,m) = size(AF)
    n != m && return false
    n == 1 && return true
    return false
end

# all(isempty, []) and any(isempty, []) have non-generic behavior.
# We do not follow it here for Eye(0).
function any(f::Function, IM::Eye{T}) where T
    d1, d2 = size(IM)
    (d1 < 1 || d2 < 1) && return false
    (d1 > 1 || d2 > 1) && return f(zero(T)) || f(one(T))
    return any(f(one(T)))
end

function all(f::Function, IM::Eye{T}) where T
    d1, d2 = size(IM)
    (d1 < 1 || d2 < 1) && return false
    (d1 > 1 || d2 > 1) && return f(zero(T)) && f(one(T))
    return all(f(one(T)))
end

# In particular, these make iszero(Eye(n))  efficient.
# use any/all on scalar to get Boolean error message
any(f::Function, x::AbstractFill) = any(f(getindex_value(x)))
all(f::Function, x::AbstractFill) = all(f(getindex_value(x)))
any(x::AbstractFill) = any(getindex_value(x))
all(x::AbstractFill) = all(getindex_value(x))

count(x::Ones{Bool}) = length(x)
count(x::Zeros{Bool}) = 0
count(f, x::AbstractFill) = f(getindex_value(x)) ? length(x) : 0

#########
# in
#########
in(x, A::AbstractFill) = x == getindex_value(A)
function in(x, A::RectDiagonal{<:Number})
    any(iszero, size(A)) && return false # Empty matrix
    all(isone, size(A)) && return x == A.diag[1] # A 1x1 matrix has only one element
    x == zero(eltype(A)) || x in A.diag
end

include("fillalgebra.jl")
include("fillbroadcast.jl")
include("trues.jl")

##
# print
##
Base.replace_in_print_matrix(::Zeros, ::Integer, ::Integer, s::AbstractString) =
    Base.replace_with_centered_mark(s)

# following support blocked fill array printing via
# BlockArrays.jl
axes_print_matrix_row(_, io, X, A, i, cols, sep) =
    Base.invoke(Base.print_matrix_row, Tuple{IO,AbstractVecOrMat,Vector,Integer,AbstractVector,AbstractString},
                io, X, A, i, cols, sep)

Base.print_matrix_row(io::IO,
        X::Union{AbstractFill{<:Any,1},
                 AbstractFill{<:Any,2},
                 Diagonal{<:Any,<:AbstractFill{<:Any,1}},
                 RectDiagonal,
                 AbstractTriangular{<:Any,<:AbstractFill{<:Any,2}}
                 }, A::Vector,
        i::Integer, cols::AbstractVector, sep::AbstractString) =
        axes_print_matrix_row(axes(X), io, X, A, i, cols, sep)

# Display concise description of a Fill.

Base.show(io::IO, x::AbstractFill) = print(io, "$(summary(x)): entries equal to $(getindex_value(x))")
Base.show(io::IO, x::Union{Zeros,Ones,Eye}) = print(io, "$(summary(x))")

if VERSION ≥ v"1.5"
    Base.array_summary(io::IO, ::Zeros{T}, inds::Tuple{Vararg{Base.OneTo}}) where T =
        print(io, Base.dims2string(length.(inds)), " Zeros{$T}")
    Base.array_summary(io::IO, ::Ones{T}, inds::Tuple{Vararg{Base.OneTo}}) where T =
        print(io, Base.dims2string(length.(inds)), " Ones{$T}")
    Base.array_summary(io::IO, a::Fill{T}, inds::Tuple{Vararg{Base.OneTo}}) where T =
        print(io, Base.dims2string(length.(inds)), " Fill{$T}")
    Base.array_summary(io::IO, a::Eye{T}, inds::Tuple{Vararg{Base.OneTo}}) where T =
        print(io, Base.dims2string(length.(inds)), " Eye{$T}")
end

Base.show(io::IO, ::MIME"text/plain", x::Union{Eye,AbstractFill}) = show(io, x)

##
# interface
##

getindex_value(a::LinearAlgebra.AdjOrTrans) = getindex_value(parent(a))
getindex_value(a::SubArray) = getindex_value(parent(a))

##
# view
##

Base.@propagate_inbounds view(A::AbstractFill{<:Any,N}, kr::AbstractArray{Bool,N}) where N = getindex(A, kr)
Base.@propagate_inbounds view(A::AbstractFill{<:Any,1}, kr::AbstractVector{Bool}) = getindex(A, kr)
Base.@propagate_inbounds view(A::AbstractFill{<:Any,N}, I::Vararg{Union{Real, AbstractArray}, N}) where N =
    getindex(A, I...)
Base.@propagate_inbounds view(A::AbstractFill{<:Any,N}, I::Vararg{Real, N}) where N =
    Base.invoke(view, Tuple{AbstractArray,Vararg{Any,N}}, A, I...)

end # module


text:
@testset "spherical triangles" begin
    Ns = 8:8:16

    RR = RealField(1024)
    results =
        RR.([
            "[12.400051652843377905286053412896636720736 +/- 1.64e-40]",
            "[13.7443552132132318354011215921380207828066502596318748941363320689579830 +/- 3.27e-71]",
            "[20.5719735379847305566258422 +/- 5.38e-26]",
            "[21.3094076301904452589534814412305177783368425771467166131131424182062385470402339419123020595676115778838298367063775989397269169412254133009366735802749167865869428407055350499081173154930 +/- 4.29e-188]",
            "[24.45691379629911169448043814477268289960795913156636922934413916 +/- 7.30e-63]",
            "[49.10994526328460991967034315150826835369842561533395606847954650063727524833998848617655899444520661743928451538721837069883497076 +/- 4.90e-129]",
            "[4.26173475529398708575 +/- 7.62e-21]",
            "[5.159145642466541711221675 +/- 3.96e-25]",
            "[6.24174833072633424 +/- 7.59e-18]",
            "[6.7771080545983009573857 +/- 5.76e-23]",
        ])

    goalradius = [2e-5, 4e-9, 5e-4, 3e-20, 2e-7, 2e-12, 1.2e-1, 2e-1, 2e-1, 9e-4]

    for i = 1:10
        domain, u, interval = MethodOfParticularSolutions.triangle(i)
        println("$i: Computing eigenvalue for the $domain")

        optim_prec_final = ifelse(i != 4, 60, 100)
        λs, _ = iteratemps(
            domain,
            u,
            interval,
            Ns,
            optim_prec_final = optim_prec_final,
            optim_prec_linear = true,
            show_trace = true,
        )
        rad = Float64(radius(λs[end]))

        @test overlaps(results[i], λs[end])
        @test rad < goalradius[i]

        if rad < goalradius[i]
            @printf "radius = %e < %e\n" rad goalradius[i]
        else
            @printf "radius = %e >= %e\n" rad goalradius[i]
        end
        println("")
    end
end


text: println("Building MyAwesomePackage in: ", __DIR__)


text:
function message(title::AbstractString, text::AbstractString)
    ccall((:IupMessage, "libiup"), Void,
          (AbstractString, AbstractString), title, text)
end


text:
using ProjAssistant
@quickactivate

# ------------------------------------------------------
@time begin
   import Chemostat_EColi
   import Chemostat_EColi: lims
   const ChE = Chemostat_EColi

   import Chemostat_InSilico
   const Dyn = Chemostat_InSilico.Dynamic
   import Chemostat_InSilico.Dynamic:
      hist, n_ave_conv!, Vi,
      load_simdat, simdat_file, get_status,
      UNDONE_SIM_STATUS, DEAD_SIM_STATUS,
      EXPLODED_SIM_STATUS, NITERS_SIM_STATUS,
      STST_SIM_STATUS, collect_ts

   using Plots
   using Plots.Measures
   import GR
   !isinteractive() && GR.inline("png")

   using Base.Threads
   using ProgressMeter
   using ColorSchemes
   using LaTeXStrings

end

# ------------------------------------------------------
function _textbf(strs...)
   strs = replace.(string.(strs), " " => "~")
   latexstring(string("\\mathbf{", strs..., "}"))
end

## ------------------------------------------------------
# Collect glucose-limited STST
include("2.1_get_glclim_stst.jl")

simid = :SimD3
params = lglob(Dyn, simid, :params, :finite_cg)
@extract params: Ds ϵs cg
is_glclim_stst = get_glclim_stst()

## ------------------------------------------------------
# Figure 1: chemostat_scheme
let
   spaces = lfig(rawdir(ChE, "chemostat_scheme.png"))
   sfig(ChE, spaces,
      "figure_1", ".png"
   )
end

## ------------------------------------------------------
# Figure 2: 2D_spaces_at_stst
let
   spaces = lfig(rawdir(ChE, "2D_spaces_at_stst.png"))
   sfig(ChE, spaces,
      "figure_2", ".png"
   )
end

## ------------------------------------------------------
# Figure 3: ME_1D_example
let
   example = lfig(rawdir(ChE, "ME_1D_example.png"))
   sfig(ChE, example,
      "figure_3", ".png"
   )
end

## ------------------------------------------------------
# Figure 4: dyn_time_series
include("2.1_time_series.jl")
let

   # time series
   Di = 8
   Xp, sgp = plot_time_series(simid, Ds[Di], ϵs, cg)

   # plot
   for p in [Xp, sgp]
      plot!(p;
         titlefont = 27,
         guidefont = 21,
         xtickfont = 21,
         ytickfont = 21,
         thickness_scaling = 1.6,
         size = (1220, 940)
      )
   end

   sfig(ChE, [Xp, sgp],
      "figure_4", ".png";
      layout = (1, 2)
   )
end

## ------------------------------------------------------
# Figure 5
include("2.3_Pol_Volume_Study.jl")
include("2.4_glucose_limiting_study.jl")

let
   ϵs_ = ϵs
   panel1 = plot_pol_vol_study(simid, Ds, ϵs_, cg)
   panel2, panel3 = plot_glc_limiting_study(simid, Ds, ϵs_, cg, is_glclim_stst)

   for (title, p) in zip('A':'Z', [panel1, panel2, panel3])
      plot!(p;
         titlefont = 22,
         guidefont = 19,
         xtickfont = 18,
         ytickfont = 18,
         legendfont = 14,
         thickness_scaling = 1.6,
         size = (1220, 940),
         title
      )
   end

   sfig(ChE, [panel1, panel2, panel3],
      "figure_5.png";
      layout = (1, 3)
   )

end

## ------------------------------------------------------
# Figure 6
include("2.5_flx_corrs.jl")

let

   ps = plot_flx_corrs(simid, Ds, ϵs, cg, is_glclim_stst)

   for p in ps
      plot!(p;
         guidefont = 26,
         xtickfont = 21,
         ytickfont = 21,
         thickness_scaling = 1.6,
         size = (1220, 900),
         rightmargin = 2mm
      )
   end

   @assert length(ps) == 3 * 6
   sfig(ChE, ps,
      "figure_6", ".png";
      layout = (6, 3)
   )

end

## ------------------------------------------------------
# Figure 7
include("2.4_plot_marginal.jl")

let
   D_ = Ds[6]
   @show D_
   ϵs_ = ϵs[1:3:end]
   ps = plot_flux_marginals3D(simid, D_, ϵs_, cg, is_glclim_stst)

   plot!(ps[1]; bottom_margin=5mm)
   plot!(ps[4]; bottom_margin=5mm)
   for p in ps
      plot!(p; titlefont = 22,
         guidefont = 22,
         xtickfont = 18,
         ytickfont = 18,
         legendfont = 14,
         thickness_scaling = 1.6,
         size = (1220, 940)
      )
   end

   layout = (2, 3)
   ffile = sfig(ChE, ps,
      "figure_7", ".png";
      layout
   )
   @info("Done", ffile)

end

## ------------------------------------------------------
# Figure 10
include("2.2_sim3D_dead_explosion_map.jl")
let
   p = plot_dead_explosion_map(simid)

   plot!(p; titlefont = 22,
      guidefont = 22,
      xtickfont = 18,
      ytickfont = 18,
      legendfont = 14,
      thickness_scaling = 1.6,
      size = (1220, 940)
   )

   ffile = sfig(ChE, p,
      "figure_10", ".png"
   )
   @info("Done", ffile)

end


text:
function write_SIM_model(filename::String,model::SimModel3D{T},grid_size) where T
  origin = model.cell_origins[1]
  a_vec = model.cell_origins[end,1,1]-origin+Point3D{T}(model.prim_vec[1,:])
  b_vec = model.cell_origins[1,end,1]-origin+Point3D{T}(model.prim_vec[2,:])
  c_vec = model.cell_origins[1,1,end]-origin+Point3D{T}(model.prim_vec[3,:])
  points = Array{WfPoint3D{T},3}(grid_size,grid_size,grid_size)
  for (ia,a) in enumerate(linspace(0,1,grid_size)),(ib,b) in enumerate(linspace(0,1,grid_size)),(ic,c) in enumerate(linspace(0,1,grid_size))
    point = origin+(a*a_vec+b*b_vec+c*c_vec)
    w = zero(Complex{T})
    for wfc in model.wfcs
      w+=wave_equation(point,wfc)
    end
    points[ia,ib,ic] = WfPoint3D{T}(w,point)
  end
  tmp_wfc = Wfc3D(points,Point3D{T}[],PhysAtom())
  write_xsf_file(filename,tmp_wfc)
end


text:
### A Pluto.jl notebook ###
# v0.12.20

using Markdown
using InteractiveUtils

# ╔═╡ 8a6e10fa-6342-11eb-2458-75f74e7ef436
using ChemEquations

# ╔═╡ 7d8bb5d6-6342-11eb-31d0-c7eedce3cc88
import Pkg

# ╔═╡ 837ac55e-6342-11eb-053e-cbf945249755
Pkg.add("ChemEquations")

# ╔═╡ c02e3a74-6342-11eb-1853-13697a19f688
md" ## _Balancing Chemical Equations_
The purpose of this notebook is to make simplifying chemical equations easier.
"

# ╔═╡ cf5071b8-6342-11eb-0716-3d6d76f3190f
md" ## Sythesis Reactions
Pattern: *A + B → AB*"

# ╔═╡ d68358ae-6342-11eb-2b53-c72fa8eee3b0
equation = ce"Na + Cl = NaCl"

# ╔═╡ 63ef87dc-6343-11eb-1440-310b02a2a426
balance(equation)

# ╔═╡ 4ea26e16-6fab-11eb-23ee-65234ffdbd25
equation1 = ce"Ba{2} + Cl{-1} = BaCl2"

# ╔═╡ e2303056-6faa-11eb-08e0-6173ba68e77a
md"$\ce{Ba{2} + Cl{-1} = BaCl2 }$"

# ╔═╡ 9bf3ccc4-6343-11eb-1d29-65720a01824a
balance(equation1)

# ╔═╡ 48dfe7ce-6344-11eb-2380-03e6256e35b7
md" ## Decomposition
Pattern: *AB → A + B *"

# ╔═╡ 54c9f35e-6344-11eb-1fd1-8fa6c1814280
equation3 = ce"MgO2 = Mg + O2"

# ╔═╡ 7d4e6ec2-6344-11eb-072c-3deba9aa7860
balance(equation3)

# ╔═╡ 3b1a565e-6f17-11eb-03fa-9b22b3f508ba
md" ## Single Replacement
Pattern: AB + C → AC + B"

# ╔═╡ 58cefddc-6f17-11eb-242d-af13426a1370
equation4= ce"Zn + HCl = ZnCl2 + H2"

# ╔═╡ 86a8f670-6f17-11eb-3ebf-878a9ff13731
balance(equation4)

# ╔═╡ 907feb04-6f17-11eb-29dc-89c905bc730a
equation5= ce"Al + Ca3(PO4)2 = AlPO4 + Ca"

# ╔═╡ a3a333de-6f18-11eb-0f92-1d97d4308fb8
balance(equation5)

# ╔═╡ d604b9c4-6f18-11eb-1ecb-9f4d26413314
md" ## Double Replacement Reaction
Pattern: AB + CD → AD + CB"

# ╔═╡ f5c1167c-6f18-11eb-20c8-1f5fb5ccb77a
equation6= ce"  KI + Pb(NO3)2 = KNO3 + PbI2"

# ╔═╡ 795e11a8-6f19-11eb-30d8-79d62ee1bf19
balance(equation6)

# ╔═╡ d07c002e-6f19-11eb-39ca-dd1b59381779
md" ## Combustion Reactions"

# ╔═╡ 1ac3c964-6f1a-11eb-1265-f5701398f990
equation7= ce" C3H8 + O2 = CO2 + H2O"

# ╔═╡ 2f2e861e-6f1a-11eb-20ac-cf441e58f4a0
balance(equation7)

# ╔═╡ 97565ffa-6f1a-11eb-110d-c5a385f50100
equation8= ce" C16H22 + O2 = CO2 + H2O"

# ╔═╡ a9e1116a-6f1a-11eb-2217-5b9b11651c30
balance(equation8)

# ╔═╡ Cell order:
# ╠═7d8bb5d6-6342-11eb-31d0-c7eedce3cc88
# ╠═837ac55e-6342-11eb-053e-cbf945249755
# ╠═8a6e10fa-6342-11eb-2458-75f74e7ef436
# ╟─c02e3a74-6342-11eb-1853-13697a19f688
# ╟─cf5071b8-6342-11eb-0716-3d6d76f3190f
# ╠═d68358ae-6342-11eb-2b53-c72fa8eee3b0
# ╠═63ef87dc-6343-11eb-1440-310b02a2a426
# ╠═4ea26e16-6fab-11eb-23ee-65234ffdbd25
# ╟─e2303056-6faa-11eb-08e0-6173ba68e77a
# ╠═9bf3ccc4-6343-11eb-1d29-65720a01824a
# ╟─48dfe7ce-6344-11eb-2380-03e6256e35b7
# ╠═54c9f35e-6344-11eb-1fd1-8fa6c1814280
# ╠═7d4e6ec2-6344-11eb-072c-3deba9aa7860
# ╟─3b1a565e-6f17-11eb-03fa-9b22b3f508ba
# ╠═58cefddc-6f17-11eb-242d-af13426a1370
# ╠═86a8f670-6f17-11eb-3ebf-878a9ff13731
# ╠═907feb04-6f17-11eb-29dc-89c905bc730a
# ╠═a3a333de-6f18-11eb-0f92-1d97d4308fb8
# ╟─d604b9c4-6f18-11eb-1ecb-9f4d26413314
# ╠═f5c1167c-6f18-11eb-20c8-1f5fb5ccb77a
# ╠═795e11a8-6f19-11eb-30d8-79d62ee1bf19
# ╟─d07c002e-6f19-11eb-39ca-dd1b59381779
# ╠═1ac3c964-6f1a-11eb-1265-f5701398f990
# ╠═2f2e861e-6f1a-11eb-20ac-cf441e58f4a0
# ╠═97565ffa-6f1a-11eb-110d-c5a385f50100
# ╠═a9e1116a-6f1a-11eb-2217-5b9b11651c30


text:
module TestDay21

import AOC2021.Day21.part1, AOC2021.Day21.part2
using Test

test_input = """
Player 1 starting position: 4
Player 2 starting position: 8"""

@testset "Day 21" begin
    @testset "part 1" begin
        @test part1(test_input) == 739785
    end
    @testset "part 2" begin
        @test part2(test_input) == 444356092776315
    end
end

end # module


text:
using Pkg
using Test

Pkg.activate("..")

using ColonyRuntime

function test_prvkey()
  prvkey = ColonyRuntime.Crypto.prvkey()
  return length(prvkey)
end

function test_id()
  id = ColonyRuntime.Crypto.id(ColonyRuntime.Crypto.prvkey())
  return length(id)
end

function test_sign()
  prvkey = "d6eb959e9aec2e6fdc44b5862b269e987b8a4d6f2baca542d8acaa97ee5e74f6"
  msg = "test_msg"
  sig = ColonyRuntime.Crypto.sign(msg, prvkey)
  return length(sig)
end

function test_hash()
  msg = "test_msg"
  hash = ColonyRuntime.Crypto.hash(msg)
  return length(hash)
end

function test_recoverid()
  msg = "test_msg"
  prvkey = ColonyRuntime.Crypto.prvkey()
  sig = ColonyRuntime.Crypto.sign(msg, prvkey)
  recoverid = ColonyRuntime.Crypto.recoverid(msg, sig)

  return recoverid == ColonyRuntime.Crypto.id(prvkey)
end

@testset begin
@test test_prvkey() == 64
@test test_id() == 64
@test test_sign() == 130
@test test_hash() == 64
@test test_recoverid()

end


text:
"""
Indexing Node attributes from node, e.g. node[:length] or node["length"]
"""
Base.getindex(node::Node, key) = unsafe_getindex(node, Symbol(key))
Base.getindex(node::Node, key::Symbol) = unsafe_getindex(node, key)

"""
Indexing a Node using an integer will index in its children
"""
Base.getindex(n::Node, i::Integer) = n.children[collect(keys(n.children))[i]]
function Base.getindex(n::Node{T,MutableNamedTuple}, i::Integer) where {T<:AbstractNodeMTG}
    n.children[collect(keys(n.children))[i]]
end

Base.setindex!(n::Node, x::Node, i::Integer) = n.children[i] = x
Base.getindex(x::Node, ::AbstractTrees.ImplicitRootState) = x

"""
Indexing Node attributes from node, e.g. node[:length] or node["length"],
but in an unsafe way, meaning it returns `nothing` when the key is not found
instead of returning an error. It is primarily used when traversing the tree,
so if a node does not have a field, it does not return an error.
"""
function unsafe_getindex(node::Node, key::Symbol)
    try
        getproperty(node.attributes, key)
    catch err
        if err.msg == "type NamedTuple has no field $key" || err.msg == "type Nothing has no field $key"
            nothing
        else
            error(err.msg)
        end
    end
end

unsafe_getindex(node::Node, key) = unsafe_getindex(node, Symbol(key))

# For a vector of keys:
unsafe_getindex(node::Node, key::Union{Vector{Symbol},Vector{String}}) = [unsafe_getindex(node, i) for i in key]
function unsafe_getindex(
    node::Node{M,T} where {M<:AbstractNodeMTG,T<:AbstractDict{Symbol,S} where {S}},
    key::Union{Vector{Symbol},Vector{String}}
)
    [unsafe_getindex(node, i) for i in key]
end

function unsafe_getindex(
    node::Node{M,T} where {M<:AbstractNodeMTG,T<:AbstractDict},
    key::Symbol
)
    get(node.attributes, key, nothing)
end

function unsafe_getindex(node::Node{M,T} where {M<:AbstractNodeMTG,T<:AbstractDict}, key)
    unsafe_getindex(node, Symbol(key))
end

Base.setindex!(node::Node{<:AbstractNodeMTG,<:AbstractDict}, x, key) = setindex!(node, x, Symbol(key))
Base.setindex!(node::Node{<:AbstractNodeMTG,<:AbstractDict}, x, key::Symbol) = node.attributes[key] = x

"""
Returns the length of the subtree below the node (including it)
"""
function Base.length(node::Node)
    i = [1]
    length_subtree(node::Node, i)
    return i[1]
end

function length_subtree(node::Node, i)
    if !isleaf(node)
        for chnode in ordered_children(node)
            i[1] = i[1] + 1
            length_subtree(chnode, i)
        end
    end
end


text:
@testset "service module" begin
    @service S3
    @test :S3 in names(Main)
end

@testset "global config, kwargs" begin
    try
        region = "us-east-2"
        AWS.global_aws_config(; region=region)

        @test AWS.global_aws_config().region == region
    finally
        AWS.aws_config[] = AWSConfig()
    end
end

@testset "set global aws config" begin
    test_region = "test region"
    expected = AWSConfig(; region=test_region)

    try
        AWS.global_aws_config(expected)
        result = AWS.global_aws_config()

        @test result.region == test_region
    finally
        AWS.global_aws_config(AWSConfig())
    end
end

@testset "set user agent" begin
    old_user_agent = AWS.user_agent[]
    new_user_agent = "new user agent"

    try
        @test AWS.user_agent[] == "AWS.jl/1.0.0"
        set_user_agent(new_user_agent)
        @test AWS.user_agent[] == new_user_agent
    finally
        set_user_agent(old_user_agent)
    end
end

@testset "sign" begin
    aws = AWS.AWSConfig(; region="us-east-1")

    access_key = "access-key"
    secret_key = "ssh... it is a secret"

    aws.credentials.access_key_id = access_key
    aws.credentials.secret_key = secret_key

    time = DateTime(2020)
    date = Dates.format(time, dateformat"yyyymmdd")

    request = Request(;
        service="s3",
        api_version="api_version",
        request_method="GET",
        headers=LittleDict(
            "Host" => "s3.us-east-1.amazonaws.com", "User-Agent" => "AWS.jl/1.0.0"
        ),
        resource="/test-resource",
        url="https://s3.us-east-1.amazonaws.com/test-resource",
    )

    @testset "sign v2" begin
        result = AWS.sign_aws2!(aws, request, time)
        content = result.content
        content_type = result.headers["Content-Type"]

        expected_access_key = "AWSAccessKeyId=$access_key"
        expected_expires = "Expires=2020-01-01T00%3A02%3A00Z"
        expected_signature_method = "SignatureMethod=HmacSHA256"
        expected_signature_version = "SignatureVersion=2"
        expected_signature = "Signature=O0MLzMKpEcfVZeHy0tyxVAuZF%2BvrvbgIGgqbWtJLTQ0%3D"

        expected_content = join(
            [
                expected_access_key,
                expected_expires,
                expected_signature_method,
                expected_signature_version,
                expected_signature,
            ],
            '&',
        )

        @test content == expected_content
    end

    @testset "sign v4" begin
        expected_x_amz_content_sha256 = bytes2hex(digest(MD_SHA256, request.content))
        expected_content_md5 = base64encode(digest(MD_MD5, request.content))
        expected_x_amz_date = Dates.format(time, dateformat"yyyymmdd\THHMMSS\Z")

        result = AWS.sign_aws4!(aws, request, time)
        headers = result.headers

        @test headers["x-amz-content-sha256"] == expected_x_amz_content_sha256
        @test headers["Content-MD5"] == expected_content_md5
        @test headers["x-amz-date"] == expected_x_amz_date

        authorization_header = split(headers["Authorization"], ' ')
        @test length(authorization_header) == 4
        @test authorization_header[1] == "AWS4-HMAC-SHA256"
        @test authorization_header[2] ==
            "Credential=$access_key/$date/us-east-1/$(request.service)/aws4_request,"
        @test authorization_header[3] ==
            "SignedHeaders=content-md5;content-type;host;user-agent;x-amz-content-sha256;x-amz-date,"
        @test authorization_header[4] ==
            "Signature=0f292eaf0b66cf353bafcb1b9b6d90ee27064236a60f17f6fc5bd7d40173a0be"
    end
end

@testset "submit_request" begin
    aws = AWS.AWSConfig()

    function _expected_xml(body::AbstractString, dict_type::Type)
        parsed = parse_xml(body)
        return xml_dict(XMLDict.root(parsed.x), dict_type)
    end

    @testset "301 redirect" begin
        request = Request(;
            service="s3",
            api_version="api_version",
            request_method="HEAD",
            url="https://s3.us-east-1.amazonaws.com/sample-bucket",
            use_response_type=true,
        )
        apply(Patches._aws_http_request_patch(Patches._response(; status=301))) do
            @test_throws AWSException AWS.submit_request(aws, request)
        end
    end

    @testset "HEAD response" begin
        request = Request(;
            service="s3",
            api_version="api_version",
            request_method="HEAD",
            url="https://s3.us-east-1.amazonaws.com/sample-bucket",
            use_response_type=true,
        )

        response = apply(Patches._aws_http_request_patch()) do
            AWS.submit_request(aws, request)
        end

        # Access to response headers
        @test response.response.headers == Patches.headers
        @test response.response.headers isa Vector

        # Access to streaming content
        @test response.io isa IO

        # Content as a string
        @test String(take!(response.io)) == Patches.body

        # Backwards compatibility with those expecting an `HTTP.Response`
        @test response.headers == Patches.headers
        @test response.headers isa Vector
        @test String(response.body) == Patches.body
    end

    @testset "GET response" begin
        request = Request(;
            service="s3",
            api_version="api_version",
            request_method="GET",
            url="https://s3.us-east-1.amazonaws.com/sample-bucket",
            use_response_type=true,
        )

        response = apply(Patches._aws_http_request_patch()) do
            AWS.submit_request(aws, request)
        end

        # Access to response headers
        @test response.response.headers == Patches.headers
        @test response.response.headers isa Vector

        # Access to streaming content
        @test response.io isa IO

        # Content as a string
        @test String(take!(response.io)) == Patches.body

        # Backwards compatibility with those expecting an `HTTP.Response`
        @test response.headers == Patches.headers
        @test response.headers isa Vector
        @test String(response.body) == Patches.body
    end

    @testset "Not authorized" begin
        request = Request(;
            service="s3",
            api_version="api_version",
            request_method="GET",
            url="https://s3.us-east-1.amazonaws.com/sample-bucket",
            use_response_type=true,
        )

        message = "User is not authorized to perform: action on resource with an explicit deny"

        # Simulate the HTTP.request behaviour with a HTTP 400 response
        exception = apply(Patches.gen_http_options_400_patches(message)) do
            try
                AWS.submit_request(aws, request)
                return nothing
            catch e
                if e isa AWSException
                    return e
                else
                    rethrow()
                end
            end
        end

        @test exception isa AWSException

        # If handled incorrectly using a `response_stream` may result in the body data being
        # lost. Mainly, this is a problem when using a temporary I/O stream instead of
        # writing directly to the `response_stream`.
        @test exception.message == message
        @test exception.streamed_body !== nothing
    end

    @testset "read MIME-type" begin
        request = Request(;
            service="s3",
            api_version="api_version",
            request_method="GET",
            url="https://s3.us-east-1.amazonaws.com/sample-bucket",
            use_response_type=true,
        )

        @testset "invalid content type" begin
            headers = Pair["Content-Type" => ""]
            body = ""
            expected_body_type = Vector{UInt8}
            expected_body = b""

            r = Patches._response(; headers=headers, body=body)
            response = apply(Patches._aws_http_request_patch(r)) do
                AWS.submit_request(aws, request)
            end

            content = parse(response)
            @test content isa expected_body_type
            @test content == expected_body
        end

        @testset "text/xml" begin
            headers = Pair["Content-Type" => "text/xml"]
            expected_body_type = LittleDict{Union{String,Symbol},Any}
            expected_body = _expected_xml(Patches.body, expected_body_type)

            r = Patches._response(; headers=headers)
            response = apply(Patches._aws_http_request_patch(r)) do
                AWS.submit_request(aws, request)
            end

            content = parse(response)
            @test content isa expected_body_type
            @test content == expected_body
        end

        @testset "application/xml" begin
            headers = Pair["Content-Type" => "application/xml"]
            expected_body_type = LittleDict{Union{String,Symbol},Any}
            expected_body = _expected_xml(Patches.body, expected_body_type)

            r = Patches._response(; headers=headers)
            response = apply(Patches._aws_http_request_patch(r)) do
                AWS.submit_request(aws, request)
            end

            content = parse(response)
            @test content isa expected_body_type
            @test content == expected_body
        end

        @testset "application/json" begin
            headers = ["Content-Type" => "application/json"]
            body = JSON.json(
                Dict{String,Any}(
                    "Marker" => nothing,
                    "VaultList" => Any[Dict{String,Any}(
                        "VaultName" => "test",
                        "SizeInBytes" => 0,
                        "NumberOfArchives" => 0,
                        "CreationDate" => "2020-06-22T03:14:41.754Z",
                        "VaultARN" => "arn:aws:glacier:us-east-1:000:vaults/test",
                        "LastInventoryDate" => nothing,
                    )],
                ),
            )

            expected_body_type = LittleDict{String,Any}
            expected_body = JSON.parse(body; dicttype=expected_body_type)

            r = Patches._response(; body=body, headers=headers)
            response = apply(Patches._aws_http_request_patch(r)) do
                AWS.submit_request(aws, request)
            end

            content = parse(response)
            @test content isa expected_body_type
            @test content == expected_body
        end

        @testset "text/html" begin
            headers = ["Content-Type" => "text/html"]
            expected_body = Patches.body

            r = Patches._response(; headers=headers)
            response = apply(Patches._aws_http_request_patch(r)) do
                AWS.submit_request(aws, request)
            end

            content = parse(response)
            @test content isa String
            @test content == expected_body
        end

        # Note: `S3.create_multipart_upload` is an example of a response type that doesn't
        # specify a Content-Type.
        @testset "missing content type" begin
            headers = Pair[]
            body = """
                <?xml version="1.0" encoding="UTF-8"?>
                <root><body>text</body></root>
                """
            expected_body_type = AbstractDict
            expected_body = Dict{String,Any}("body" => "text")

            r = Patches._response(; headers=headers, body=body)
            response = apply(Patches._aws_http_request_patch(r)) do
                AWS.submit_request(aws, request)
            end

            content = parse(response)
            @test content isa expected_body_type
            @test content == expected_body

            content = parse(response, MIME"text/plain"())
            @test content isa String
            @test content == body
        end
    end
end

struct TestBackend <: AWS.AbstractBackend
    param::Int
end

function AWS._http_request(backend::TestBackend, ::AWS.Request, ::IO)
    return backend.param
end

@testset "HTTPBackend" begin
    request = Request(;
        service="s3",
        api_version="api_version",
        request_method="GET",
        url="https://s3.us-east-1.amazonaws.com/sample-bucket",
        backend=AWS.HTTPBackend(),
    )
    io = IOBuffer()

    apply(Patches._http_options_patches) do
        # No default options
        @test isempty(AWS._http_request(request.backend, request, io))

        # We can pass HTTP options via the backend
        custom_backend = AWS.HTTPBackend(Dict(:connection_limit => 5))
        @test custom_backend isa AWS.AbstractBackend
        @test AWS._http_request(custom_backend, request, io) == Dict(:connection_limit => 5)

        # We can pass options per-request
        request.http_options = Dict(:pipeline_limit => 20)
        @test AWS._http_request(request.backend, request, io) == Dict(:pipeline_limit => 20)
        @test AWS._http_request(custom_backend, request, io) ==
            Dict(:pipeline_limit => 20, :connection_limit => 5)

        # per-request options override backend options:
        custom_backend = AWS.HTTPBackend(Dict(:pipeline_limit => 5))
        @test AWS._http_request(custom_backend, request, io) == Dict(:pipeline_limit => 20)
    end

    request.backend = TestBackend(2)
    @test AWS._http_request(request.backend, request, io) == 2

    request = Request(;
        service="s3",
        api_version="api_version",
        request_method="GET",
        url="https://s3.us-east-1.amazonaws.com/sample-bucket",
        backend=TestBackend(4),
    )
    @test AWS._http_request(request.backend, request, io) == 4

    # Let's test setting the default backend
    prev_backend = AWS.DEFAULT_BACKEND[]
    try
        AWS.DEFAULT_BACKEND[] = TestBackend(3)
        request = Request(;
            service="s3",
            api_version="api_version",
            request_method="GET",
            url="https://s3.us-east-1.amazonaws.com/sample-bucket",
        )
        @test AWS._http_request(request.backend, request, io) == 3
    finally
        AWS.DEFAULT_BACKEND[] = prev_backend
    end
end

@testset "_generate_rest_resource" begin
    request_uri = "/{Bucket}/{Key+}"
    args = Dict{String,Any}("Bucket" => "aws.jl-test", "Key" => "Test-Key")

    expected = "/$(args["Bucket"])/$(args["Key"])"
    result = AWS._generate_rest_resource(request_uri, args)
    @test result == expected
end

@testset "generate_service_url" begin
    region = "us-east-2"
    resource = "/aws.jl-test---timestamp"
    config = AWSConfig()
    config.region = region

    request = Request(;
        service="service",
        api_version="api_version",
        request_method="GET",
        resource=resource,
    )

    @testset "regionless endpoints" for regionless_endpoint in ("iam", "route53")
        endpoint = "sdb"
        request.service = regionless_endpoint
        expected_result = "https://$regionless_endpoint.amazonaws.com$resource"
        result = AWS.generate_service_url(config, request.service, request.resource)

        @test result == expected_result
    end

    @testset "region service" begin
        endpoint = "sdb"
        request.service = endpoint
        expected_result = "https://$endpoint.$region.amazonaws.com$resource"
        result = AWS.generate_service_url(config, request.service, request.resource)

        @test result == expected_result
    end

    @testset "sdb -- us-east-1 region exception" begin
        endpoint = "sdb"
        request.service = endpoint
        expected_result = "https://$endpoint.amazonaws.com$resource"
        config.region = "us-east-1"
        result = AWS.generate_service_url(config, request.service, request.resource)

        @test result == expected_result
    end
end

@testset "_flatten_query" begin
    high_level_value = "high_level_value"
    entry_1 = LittleDict(
        "low_level_key_1" => "low_level_value_1", "low_level_key_2" => "low_level_value_2"
    )
    entry_2 = LittleDict(
        "low_level_key_3" => "low_level_value_3", "low_level_key_4" => "low_level_value_4"
    )

    args = LittleDict(
        "high_level_key" => high_level_value, "high_level_array" => [entry_1, entry_2]
    )

    @testset "non-special case suffix" begin
        service = "sts"
        result = AWS._flatten_query(service, args)

        expected = Pair{String,String}[
            "high_level_key" => "high_level_value",
            "high_level_array.member.1.low_level_key_1" => "low_level_value_1",
            "high_level_array.member.1.low_level_key_2" => "low_level_value_2",
            "high_level_array.member.2.low_level_key_3" => "low_level_value_3",
            "high_level_array.member.2.low_level_key_4" => "low_level_value_4",
        ]

        @test result == expected
    end

    @testset "sqs - special casing suffix" begin
        service = "sqs"
        result = AWS._flatten_query(service, args)

        expected = Pair{String,String}[
            "high_level_key" => "high_level_value",
            "high_level_array.1.low_level_key_1" => "low_level_value_1",
            "high_level_array.1.low_level_key_2" => "low_level_value_2",
            "high_level_array.2.low_level_key_3" => "low_level_value_3",
            "high_level_array.2.low_level_key_4" => "low_level_value_4",
        ]

        @test result == expected
    end
end

@testset "_clean_s3_uri" begin
    uri = "/test-bucket/*)=('! +@,:.txt?list-objects=v2"
    expected_uri = "/test-bucket/%2A%29%3D%28%27%21%20%2B%40%2C%3A.txt?list-objects=v2"
    @test AWS._clean_s3_uri(uri) == expected_uri

    # make sure that other parts of the uri aren't changed by `_clean_s3_uri`
    for uri in (
        "https://julialang.org",
        "http://julialang.org",
        "http://julialang.org:8080",
        "/onlypath",
        "/path?query=  +99",
        "/anchor?query=yes#anchor1",
    )
        @test AWS._clean_s3_uri(uri) == uri
    end
end

@testset "STS" begin
    @testset "high-level" begin
        @service STS

        response = STS.get_caller_identity()
        d = response["GetCallerIdentityResult"]

        @test Set(keys(d)) == Set(["Arn", "UserId", "Account"])
        @test occursin(r"^arn:aws:(iam|sts):", d["Arn"])
        @test all(isdigit, d["Account"])
    end

    @testset "low-level" begin
        response = AWSServices.sts("GetCallerIdentity")
        d = response["GetCallerIdentityResult"]

        @test Set(keys(d)) == Set(["Arn", "UserId", "Account"])
        @test occursin(r"^arn:aws:(iam|sts):", d["Arn"])
        @test all(isdigit, d["Account"])
    end
end

@testset "json" begin
    @testset "high-level secrets manager" begin
        @service Secrets_Manager

        secret_name = "aws-jl-test---" * _now_formatted()
        secret_string = "sshhh it is a secret!"

        function _get_secret_string(secret_name)
            response = Secrets_Manager.get_secret_value(secret_name)

            return response["SecretString"]
        end

        Secrets_Manager.create_secret(
            secret_name,
            LittleDict(
                "SecretString" => secret_string, "ClientRequestToken" => string(uuid4())
            ),
        )

        try
            @test _get_secret_string(secret_name) == secret_string
        finally
            Secrets_Manager.delete_secret(
                secret_name, LittleDict("ForceDeleteWithoutRecovery" => "true")
            )
        end

        @test_throws AWSException _get_secret_string(secret_name)
    end

    @testset "low-level secrets manager" begin
        secret_name = "aws-jl-test---" * _now_formatted()
        secret_string = "sshhh it is a secret!"

        function _get_secret_string(secret_name)
            response = AWSServices.secrets_manager(
                "GetSecretValue", LittleDict("SecretId" => secret_name)
            )

            return response["SecretString"]
        end

        resp = AWSServices.secrets_manager(
            "CreateSecret",
            LittleDict(
                "Name" => secret_name,
                "SecretString" => secret_string,
                "ClientRequestToken" => string(uuid4()),
            ),
        )

        try
            @test _get_secret_string(secret_name) == secret_string
        finally
            AWSServices.secrets_manager(
                "DeleteSecret",
                LittleDict(
                    "SecretId" => secret_name, "ForceDeleteWithoutRecovery" => "true"
                ),
            )
        end

        @test_throws AWSException _get_secret_string(secret_name)
    end
end

@testset "query" begin
    @testset "high-level iam" begin
        @service IAM

        policy_arn = ""
        expected_policy_name = "aws-jl-test---" * _now_formatted()
        expected_policy_document = LittleDict(
            "Version" => "2012-10-17",
            "Statement" => [
                LittleDict(
                    "Effect" => "Allow",
                    "Action" => ["s3:Get*", "s3:List*"],
                    "Resource" => ["arn:aws:s3:::my-bucket/shared/*"],
                ),
            ],
        )
        expected_policy_document = JSON.json(expected_policy_document)

        response = IAM.create_policy(expected_policy_document, expected_policy_name)
        policy_arn = response["CreatePolicyResult"]["Policy"]["Arn"]

        try
            response_policy_version = IAM.get_policy_version(policy_arn, "v1")
            response_document = response_policy_version["GetPolicyVersionResult"]["PolicyVersion"]["Document"]
            @test HTTP.unescapeuri(response_document) == expected_policy_document
        finally
            IAM.delete_policy(policy_arn)
        end

        @test_throws AWSException IAM.get_policy(policy_arn)
    end

    @testset "low-level iam" begin
        policy_arn = ""
        expected_policy_name = "aws-jl-test---" * _now_formatted()
        expected_policy_document = LittleDict(
            "Version" => "2012-10-17",
            "Statement" => [
                LittleDict(
                    "Effect" => "Allow",
                    "Action" => ["s3:Get*", "s3:List*"],
                    "Resource" => ["arn:aws:s3:::my-bucket/shared/*"],
                ),
            ],
        )
        expected_policy_document = JSON.json(expected_policy_document)

        response = AWSServices.iam(
            "CreatePolicy",
            LittleDict(
                "PolicyName" => expected_policy_name,
                "PolicyDocument" => expected_policy_document,
            ),
        )
        policy_arn = response["CreatePolicyResult"]["Policy"]["Arn"]

        try
            response_policy_version = AWSServices.iam(
                "GetPolicyVersion",
                LittleDict("PolicyArn" => policy_arn, "VersionId" => "v1"),
            )
            response_document = response_policy_version["GetPolicyVersionResult"]["PolicyVersion"]["Document"]
            @test HTTP.unescapeuri(response_document) == expected_policy_document
        finally
            AWSServices.iam("DeletePolicy", LittleDict("PolicyArn" => policy_arn))
        end

        @test_throws AWSException AWSServices.iam(
            "GetPolicy", LittleDict("PolicyArn" => policy_arn)
        )
    end

    @testset "high-level sqs" begin
        @service SQS

        queue_name = "aws-jl-test---" * _now_formatted()
        expected_message = "Hello for AWS.jl"

        function _get_queue_url(queue_name)
            result = SQS.get_queue_url(queue_name)

            return result["GetQueueUrlResult"]["QueueUrl"]
        end

        # Create Queue
        SQS.create_queue(queue_name)
        queue_url = _get_queue_url(queue_name)

        try
            # Get Queues
            @test !isempty(queue_url)

            # Change Message Visibility Batch Request
            expected_message_id = "aws-jl-test"

            SQS.send_message(expected_message, queue_url)

            response = SQS.receive_message(queue_url)
            receipt_handle = response["ReceiveMessageResult"]["Message"]["ReceiptHandle"]

            response = SQS.delete_message_batch(
                [
                    LittleDict(
                        "Id" => expected_message_id, "ReceiptHandle" => receipt_handle
                    ),
                ],
                queue_url,
            )

            message_id = response["DeleteMessageBatchResult"]["DeleteMessageBatchResultEntry"]["Id"]
            @test message_id == expected_message_id

            SQS.send_message(expected_message, queue_url)

            result = SQS.receive_message(queue_url)
            message = result["ReceiveMessageResult"]["Message"]["Body"]
            @test message == expected_message
        finally
            SQS.delete_queue(queue_url)
        end

        @test_throws AWSException _get_queue_url(queue_name)
    end

    @testset "low-level sqs" begin
        queue_name = "aws-jl-test---" * _now_formatted()
        expected_message = "Hello for AWS.jl"

        function _get_queue_url(queue_name)
            result = AWSServices.sqs("GetQueueUrl", LittleDict("QueueName" => queue_name))

            return result["GetQueueUrlResult"]["QueueUrl"]
        end

        # Create Queue
        AWSServices.sqs("CreateQueue", LittleDict("QueueName" => queue_name))

        queue_url = _get_queue_url(queue_name)
        @test !isempty(queue_url)

        try
            # Change Message Visibility Batch Request
            expected_message_id = "aws-jl-test"

            AWSServices.sqs(
                "SendMessage",
                LittleDict("QueueUrl" => queue_url, "MessageBody" => expected_message),
            )

            response = AWSServices.sqs(
                "ReceiveMessage", LittleDict("QueueUrl" => queue_url)
            )
            receipt_handle = response["ReceiveMessageResult"]["Message"]["ReceiptHandle"]

            response = AWSServices.sqs(
                "DeleteMessageBatch",
                LittleDict(
                    "QueueUrl" => queue_url,
                    "DeleteMessageBatchRequestEntry" => [
                        LittleDict(
                            "Id" => expected_message_id,
                            "ReceiptHandle" => receipt_handle,
                        ),
                    ],
                ),
            )

            message_id = response["DeleteMessageBatchResult"]["DeleteMessageBatchResultEntry"]["Id"]
            @test message_id == expected_message_id

            # Send message
            AWSServices.sqs(
                "SendMessage",
                LittleDict("QueueUrl" => queue_url, "MessageBody" => expected_message),
            )

            # Receive Message
            result = AWSServices.sqs("ReceiveMessage", LittleDict("QueueUrl" => queue_url))
            message = result["ReceiveMessageResult"]["Message"]["Body"]
            @test message == expected_message
        finally
            AWSServices.sqs("DeleteQueue", LittleDict("QueueUrl" => queue_url))
        end

        @test_throws AWSException _get_queue_url(queue_name)
    end
end

@testset "rest-xml" begin
    @testset "high-level s3" begin
        @service S3

        bucket_name = "aws-jl-test---" * _now_formatted()
        file_name = string(uuid4())

        function _bucket_exists(bucket_name)
            try
                S3.head_bucket(bucket_name)
                return true
            catch e
                if e isa AWSException && e.cause.status == 404
                    return false
                else
                    rethrow(e)
                end
            end
        end

        # HEAD operation
        @test _bucket_exists(bucket_name) == false

        # PUT operation
        S3.create_bucket(bucket_name)
        @test _bucket_exists(bucket_name)

        try
            # PUT with parameters operation
            body = "sample-file-body"
            S3.put_object(bucket_name, file_name, Dict("body" => body))
            @test !isempty(S3.get_object(bucket_name, file_name))

            # GET operation
            result = S3.list_objects(bucket_name)
            @test result["Contents"]["Key"] == file_name

            # GET with parameters operation
            max_keys = 1
            result = S3.list_objects(bucket_name, Dict("max_keys" => max_keys))
            @test length([result["Contents"]]) == max_keys

            # GET with an IO target
            mktemp() do f, io
                S3.get_object(bucket_name, file_name, Dict("response_stream" => io))
                flush(io)
                @test read(f, String) == body
            end
        finally
            # DELETE with parameters operation
            S3.delete_object(bucket_name, file_name)
            @test_throws AWSException S3.get_object(bucket_name, file_name)

            # DELETE operation
            S3.delete_bucket(bucket_name)

            sleep(2)
        end

        @test _bucket_exists(bucket_name) == false
    end

    @testset "low-level s3" begin
        bucket_name = "aws-jl-test---" * _now_formatted()
        file_name = "*)=('! +@,:.txt"  # Special characters which S3 allows

        function _bucket_exists(bucket_name)
            try
                AWSServices.s3("HEAD", "/$bucket_name")
                return true
            catch e
                if e isa AWSException && e.cause.status == 404
                    return false
                else
                    rethrow(e)
                end
            end
        end

        # HEAD operation
        @test _bucket_exists(bucket_name) == false

        # PUT operation
        AWSServices.s3("PUT", "/$bucket_name")
        @test _bucket_exists(bucket_name)

        try
            # PUT with parameters operation
            body = Array{UInt8}("sample-file-body")
            AWSServices.s3("PUT", "/$bucket_name/$file_name", Dict("body" => body))
            @test AWSServices.s3("GET", "/$bucket_name/$file_name") == body

            # GET operation
            result = AWSServices.s3("GET", "/$bucket_name")
            @test result["Contents"]["Key"] == file_name

            # GET with parameters operation
            max_keys = 1
            result = AWSServices.s3("GET", "/$bucket_name", Dict("max_keys" => max_keys))
            @test length([result["Contents"]]) == max_keys

            # POST with parameters operation
            body = """
                <Delete xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
                    <Object>
                        <Key>$file_name</Key>
                    </Object>
                </Delete>
                """

            AWSServices.s3("POST", "/$bucket_name?delete", Dict("body" => body))
            @test_throws AWSException AWSServices.s3("GET", "/$bucket_name/$file_name")
        finally
            # DELETE operation
            AWSServices.s3("DELETE", "/$bucket_name")

            sleep(2)
        end

        @test _bucket_exists(bucket_name) == false
    end
end

@testset "rest-json" begin
    @testset "high-level glacier" begin
        @service Glacier

        timestamp = _now_formatted()
        vault_names = ["aws-jl-test-01---$timestamp", "aws-jl-test-02---$timestamp"]

        # PUT
        for vault in vault_names
            Glacier.create_vault("-", vault)
        end

        try
            # POST
            tags = Dict("Tags" => LittleDict("Tag-01" => "Tag-01", "Tag-02" => "Tag-02"))

            for vault in vault_names
                Glacier.add_tags_to_vault("-", vault, tags)
            end

            for vault in vault_names
                result_tags = Glacier.list_tags_for_vault("-", vault)
                @test result_tags == tags
            end

            # GET
            # If this is an Integer AWS Coral cannot convert it to a String
            # "class com.amazon.coral.value.json.numbers.TruncatingBigNumber can not be converted to an String"
            limit = "1"
            args = LittleDict("limit" => limit)
            result = Glacier.list_vaults("-", args)
            @test length(result["VaultList"]) == parse(Int, limit)
        finally
            # DELETE
            for vault in vault_names
                Glacier.delete_vault("-", vault)
            end
        end

        result = Glacier.list_vaults("-")
        res_vault_names = [v["VaultName"] for v in result["VaultList"]]

        for vault in vault_names
            @test !(vault in res_vault_names)
        end
    end

    @testset "low-level glacier" begin
        timestamp = _now_formatted()
        vault_names = ["aws-jl-test-01---$timestamp", "aws-jl-test-02---$timestamp"]

        # PUT
        for vault in vault_names
            AWSServices.glacier("PUT", "/-/vaults/$vault")
        end

        try
            # POST
            tags = Dict("Tags" => LittleDict("Tag-01" => "Tag-01", "Tag-02" => "Tag-02"))

            for vault in vault_names
                AWSServices.glacier("POST", "/-/vaults/$vault/tags?operation=add", tags)
            end

            for vault in vault_names
                result_tags = AWSServices.glacier("GET", "/-/vaults/$vault/tags")

                @test result_tags == tags
            end

            # GET
            # If this is an Integer AWS Coral cannot convert it to a String
            # "class com.amazon.coral.value.json.numbers.TruncatingBigNumber can not be converted to an String"
            limit = "1"
            params = LittleDict("limit" => limit)
            result = AWSServices.glacier("GET", "/-/vaults/", params)

            @test length(result["VaultList"]) == parse(Int, limit)
        finally
            # DELETE
            for vault in vault_names
                AWSServices.glacier("DELETE", "/-/vaults/$vault")
            end
        end

        result = AWSServices.glacier("GET", "/-/vaults")
        res_vault_names = [v["VaultName"] for v in result["VaultList"]]

        for vault in vault_names
            @test !(vault in res_vault_names)
        end
    end
end


text:
using GeoDataFrames, DataFrames, ArchGDAL
using Base.Test

const testdatadir = dirname(@__FILE__)

REPO_URL = "https://github.com/yeesian/ArchGDALDatasets/blob/master/"

remotefiles = [
    "data/point.geojson"
]

for f in remotefiles
    # create the directories if they don't exist
    currdir = dirname(f)
    isdir(currdir) || mkpath(currdir)
    # download the file
    currfile = joinpath(testdatadir, f)
    isfile(currfile) || download(REPO_URL*f*"?raw=true", currfile)
end

df = GeoDataFrames.read("data/point.geojson")
@test DataFrames.nrow(df) == 4
@test df[:pointname] == ["point-a", "point-b", "a", "b"]
@test df[:FID] == [2.0, 3.0, 0.0, 3.0]
@test ArchGDAL.toWKT.(df[:geometry0]) == [
    "POINT (100 0)",
    "POINT (100.2785 0.0893)",
    "POINT (100 0)",
    "POINT (100.2785 0.0893)"
]

for f in remotefiles
    currfile = joinpath(testdatadir, f)
    isfile(currfile) && rm(currfile)
end