using FITSIO
using CFITSIO

# Deal with compatibility issues.
using Test
using Random # for `randstring`

@testset "Images" begin
    # Create a FITS instance and loop over supported types.
    fname = tempname() * ".fits"
    FITS(fname, "w") do f
        for T in (UInt8, Int8, UInt16, Int16, UInt32, Int32, Int64,
                  Float32, Float64)
            indata = reshape(T[1:100;], 5, 20)

            # Test writing the data to a new extension
            write(f, indata)

            # test reading the full array
            outdata = read(f[end])
            @test indata == outdata
            @test eltype(indata) == eltype(outdata)

            # test reading subsets of the array
            @test read(f[end], :, :) == indata
            @test read(f[end], 4, 1:10) == indata[4, 1:10]  # 2-d array
            @test read(f[end], :, 4) == indata[:, 4]  # 1-d array
            @test read(f[end], 2, 3) == indata[2, 3]  # scalar
            @test read(f[end], :, 1:2:10) == indata[:, 1:2:10]
            @test read(f[end], 1:3, :) == indata[1:3, :]

            # test expected errors
            @test_throws DimensionMismatch read(f[end], :)
            @test_throws DimensionMismatch read(f[end], :, :, 1)
            @test_throws BoundsError read(f[end], 1:6, :)
            @test_throws BoundsError read(f[end], 1, 0)

        end

        @test_throws ErrorException f[100]

        # Test representation
        @test repr(f)[end-17:end] == "9          Image  "
        @test repr(f[1])[1:6] == "File: "

        # test iteration
        for hdu in f
            @test size(hdu) == (5, 20)
        end
    end
    rm(fname, force=true)

    # copy_section()
    fname1 = tempname() * ".fits"
    fname2 = tempname() * ".fits"
    try
        f1 = FITS(fname1, "w")
        indata = reshape(Float32[1:400;], 20, 20)
        write(f1, indata)

        f2 = FITS(fname2, "w")
        copy_section(f1[1], f2, 1:10, 1:10)
        copy_section(f1[1], f2, 1:10, 1:2:20)
        outdata = read(f2[1])
        @test outdata == indata[1:10, 1:10]
        outdata = read(f2[2])
        @test outdata == indata[1:10, 1:2:20]
        close(f1)
        close(f2)
    finally
        rm(fname1, force=true)
        rm(fname2, force=true)
    end

    @testset "non-allocating read" begin
        fname1 = tempname() * ".fits"
        try
            FITS(fname1, "r+") do f

                indata = reshape(Float32[1:400;], 20, 20)
                write(f, indata)

                indata3D = reshape([1:30;], 3, 2, 5)
                write(f, indata3D)

                # Read the entire array using read to compare with read!
                a = read(f[1])
                a3D = read(f[2])

                # Read the entire array
                b = zeros(eltype(indata),size(indata))
                read!(f[1],b)
                @test a == b
                read!(f[1],b,:,:)
                @test a == b

                # Read a 2D slice
                b = zeros(eltype(indata),2,2)
                read!(f[1],b,1:2,1:2)
                @test a[1:2,1:2] == b

                # Read an entire 1D slice
                b = zeros(eltype(indata),size(indata,1))
                read!(f[1],b,:,1)
                @test a[:,1] == b

                b = zeros(eltype(indata),size(indata,2))
                read!(f[1],b,1,:)
                @test a[1,:] == b

                # Read a part of a 1D slice
                b = zeros(eltype(indata),1)
                read!(f[1],b,1:1,1)
                @test a[1,1] == b[1]
                read!(f[1],b,1,1:1)
                @test a[1,1] == b[1]

                # Read a single element into a 0-dim array
                b = zeros(eltype(indata))
                read!(f[1],b,1,1)
                @test a[1,1] == first(b)

                # Test for errors
                b = zeros(Float64)
                # Type is checked before dimensions
                @test_throws TypeError read!(f[1],b,1,1)
                @test_throws TypeError read!(f[1],b)

                b = zeros(eltype(indata))
                @test_throws DimensionMismatch read!(f[1],b,1:10,1)
                @test_throws DimensionMismatch read!(f[1],b,1:10,1:10)
                @test_throws DimensionMismatch read!(f[1],b)
                @test_throws DimensionMismatch read!(f[1],b,:,1)
                @test_throws DimensionMismatch read!(f[1],b,1,:)
                @test_throws DimensionMismatch read!(f[1],b,:,:)

                b = zeros(eltype(indata),1)
                @test_throws DimensionMismatch read!(f[1],b,1,1)

                b3D = zeros(eltype(indata3D),size(indata3D))
                read!(f[2],b3D)
                @test a3D == b3D
                read!(f[2],b3D,:,:,:)
                @test a3D == b3D

                b0D = zeros(eltype(indata))
                read!(f[1],b0D,1,1)
                @test a[1,1] == b0D[]

                @testset "read into views" begin

                    b = zeros(eltype(indata),size(indata))

                    # Entire array
                    b_view = @view b[:,:]
                    read!(f[1],b_view)
                    @test a == b

                    b_view = @view b3D[:,:,:]
                    read!(f[2],b_view)
                    @test a3D == b3D

                    @testset "1D slices of a 2D array" begin
                        for ax2 in axes(b,2)
                            b .= zero(eltype(b))
                            b_view = @view b[:,ax2]
                            read!(f[1],b_view,:,ax2)
                            @test a[:,ax2] == b[:,ax2]
                        end

                        # Non-contiguous views can not be read into
                        b .= zero(eltype(b))
                        b_view = @view b[1,:]
                        @test_throws ArgumentError read!(f[1],b_view,1,:)
                    end

                    @testset "1D slices of a 3D array" begin
                        for ax2 in axes(b3D,2), ax3 in axes(b3D,3)
                            b3D .= zero(eltype(b3D))
                            b_view = @view b3D[:,ax2,ax3]
                            read!(f[2],b_view,:,ax2,ax3)
                            @test a3D[:,ax2,ax3] == b3D[:,ax2,ax3]
                        end

                        # Non-contiguous views can not be read into
                        b3D .= zero(eltype(b3D))
                        b_view = @view b3D[1,1,:]
                        @test_throws ArgumentError read!(f[2],b_view,1,1,:)

                        b_view = @view b3D[1,:,1]
                        @test_throws ArgumentError read!(f[2],b_view,1,:,1)
                    end

                    @testset "2D slices of a 3D array" begin
                        for ax3 in axes(b3D,3)
                            b3D .= zero(eltype(b3D))
                            b_view = @view b3D[:,:,ax3]
                            read!(f[2],b_view,:,:,ax3)
                            @test a3D[:,:,ax3] == b3D[:,:,ax3]
                        end

                        # Non-contiguous views can not be read into
                        b3D .= zero(eltype(b3D))
                        b_view = @view b3D[:,1,:]
                        @test_throws ArgumentError read!(f[2],b_view,:,1,:)

                        b_view = @view b3D[1,:,:]
                        @test_throws ArgumentError read!(f[2],b_view,1,:,:)
                    end

                    @testset "0D view" begin
                        b .= zero(eltype(b))
                        b_view = @view b[1,1]
                        read!(f[1],b_view,1,1)
                        @test a[1,1] == b[1,1]
                    end
                end
            end
        finally
            rm(fname1, force=true)
        end
    end

    @testset "reinterpreted complex" begin
        @testset "read" begin
            fname = tempname() * ".fits"

            function _testreadcomplex(arr::AbstractArray{Complex{T}}) where T
                FITS(fname,"w") do f
                    write(f, collect(reinterpret(T,arr)) )
                end
                b = similar(arr)
                FITS(fname,"r") do f
                    read!(f[1], reinterpret(T,b))
                end
                @test b == arr
                c = FITS(fname,"r") do f
                    read(f[1])
                end
                @test c == reinterpret(T,arr)
            end

            function testreadcomplex(arr::Array{Complex{T}}) where T
                # Given a complex array, reinterpret it as float and write to file
                # Read it back and compare
                _testreadcomplex(arr)

                # Write a contiguous subsection instead of the entire array
                region = 1:size(arr,1)
                arrv = @view arr[region]
                _testreadcomplex(arrv)
            end

            try
                testreadcomplex(ones(ComplexF64,3))
                testreadcomplex(ones(ComplexF64,3,4))
                testreadcomplex(ones(ComplexF64,3,4,5))
            finally
                rm(fname,force=true)
            end
        end

        @testset "write" begin
            fname = tempname() * ".fits"

            function _testwritecomplex(arr::AbstractArray{Complex{T}}) where T
                FITS(fname,"w") do f
                    write(f, reinterpret(T,arr) )
                end
                a = FITS(fname,"r") do f
                    read(f[1])
                end
                b = collect(reinterpret(T,arr))
                @test b == a
                c = FITS(fname,"r") do f
                    reinterpret(Complex{T},read(f[1]))
                end
                @test c == arr
            end

            function testwritecomplex(arr::Array{Complex{T}}) where T
                # Given a complex array, reinterpret it as float and write to file
                # Read it back and compare
                _testwritecomplex(arr)

                # Write a subsection instead of the entire array
                region = 1:size(arr,1)
                arrv = @view arr[region]
                _testwritecomplex(arrv)
            end

            try
                testwritecomplex(ones(ComplexF64,3))
                testwritecomplex(ones(ComplexF64,3,4))
                testwritecomplex(ones(ComplexF64,3,4,5))
            finally
                rm(fname,force=true)
            end
        end
    end

    @testset "write views to file" begin
        fname = tempname() * ".fits"
        b = reshape([1:8;],2,2,2)

        @testset "2D slice of a 3D array" begin
            try
                FITS(fname,"r+") do f
                    for (ind,ax3) in enumerate(axes(b,3))
                        b_view = @view b[:,:,ax3]
                        write(f,b_view)
                        @test read(f[ind]) == b_view
                    end
                end
            finally
                rm(fname)
            end
        end

        @testset "1D slice of a 3D array" begin
            try
                FITS(fname,"r+") do f
                    ax23iter = Iterators.product(axes(b)[2:3]...)
                    for (ind,(ax2,ax3)) in enumerate(ax23iter)
                        b_view = @view b[:,ax2,ax3]
                        write(f,b_view)
                        @test read(f[ind]) == b_view
                    end
                end
            finally
                rm(fname)
            end
        end

        @testset "Non-contiguous" begin
            try
                FITS(fname,"r+") do f
                    b_view = @view b[:,1,:]
                    @test_throws ArgumentError write(f,b_view)

                    b_view = @view b[1,:,:]
                    @test_throws ArgumentError write(f,b_view)

                    b_view = @view b[1,1,:]
                    @test_throws ArgumentError write(f,b_view)

                    b_view = @view b[1,:,1]
                    @test_throws ArgumentError write(f,b_view)

                    # write something random to avoid the error on closing
                    write(f,zeros(1))
                end
            finally
                rm(fname)
            end
        end
    end
end

@testset "Write data to an existing image HDU" begin
    @testset "Overwrite an image" begin
        # Create a file with two images
        fname = tempname() * ".fits"
        FITS(fname, "w") do f
            write(f, [[1 2 3]; [4 5 6]])
            write(f, [[7 8 9]; [17 52 10]])
        end

        # Open the file in read-write mode
        FITS(fname, "r+") do f
            @test read(f[1]) == [[1 2 3]; [4 5 6]]
            @test read(f[2]) == [[7 8 9]; [17 52 10]]

            # Write data into the first image HDU
            write(f[1], [[11 12 13]; [14 15 16]])

            @test read(f[1]) == [[11 12 13]; [14 15 16]]  # First HDU is updated
            @test read(f[2]) == [[7 8 9]; [17 52 10]]  # Second HDU is untouched
        end

        rm(fname, force=true)
    end

    @testset "Show size mismatch error" begin
        fname = tempname() * ".fits"
        FITS(fname, "w") do f
            write(f, [[1 2 3 4]; [5 6 7 8]])
            write(f, [[1 2 3]; [4 5 6]])
        end

        FITS(fname, "r+") do f
            second_hdu = f[2]  # Get the second HDU

            # Make the first HDU to be the current HDU
            # Needed to check if `write` method compares data size with the given HDU
            # and not with the current HDU
            fits_movabs_hdu(f.fitsfile, 1)

            # Attempt to write data array of incorrect size to the second HDU
            exception = ErrorException("size of HDU [2, 3] not equal to size of data [2, 4].")
            @test_throws exception write(second_hdu, [[11 12 13 14]; [15 16 17 18]])
        end

        rm(fname, force=true)
    end
end

@testset "Tables" begin
    fname = tempname() * ".fits"
    f = FITS(fname, "w")

    ## Binary table
    indata = Dict{String, Array}()
    for (i, T) in enumerate([UInt8, Int8, UInt16, Int16, UInt32, Int32, Int64,
                             Float32, Float64, ComplexF32, ComplexF64])
        indata["col$i"] = T[1:20;]
    end
    i = length(indata) + 1
    indata["col$i"] = [randstring(10) for j=1:20]  # ASCIIString column
    i += 1
    indata["col$i"] = ones(Bool, 20)  # Bool column
    i += 1
    indata["col$i"] = reshape([1:40;], (2, 20))  # vector Int64 column
    i += 1
    indata["col$i"] = [randstring(5) for j=1:2, k=1:20]  # vector ASCIIString col
    indata["vcol"] = [randstring(j) for j=1:20]  # variable length column
    indata["VCOL"] = [collect(1.:j) for j=1.:20.] # variable length

    # test writing
    write(f, indata; varcols=["vcol", "VCOL"])

    # test reading
    colnames = FITSIO.colnames(f[2])
    for (colname, incol) in indata
        outcol = read(f[2], colname)  # table is in extension 2 (1 = primary hdr)
        @test outcol == incol
        @test eltype(outcol) == eltype(incol)
        @test colname in colnames
    end

    # TODO: remove the tests for deprecation warnings when we don't issue them, but keep the
    # `@test_throws` test.
    @test_throws ErrorException @test_nowarn(read(f[2], "vcol", case_sensitive=false))
    @test_nowarn read(f[2], "col2")
    @test_logs (:warn, r"case_sensitive") read(f[2], "COL2")

    # Test representation
    @test repr(f[2])[end-38:end] == "\n\n         (*) = variable-length column"

    ## ASCII tables

    indata = Dict{String, Array}()
    for (i, T) in enumerate([Int16, Int32, Float32, Float64])
        indata["col$i"] = T[1:20;]
    end
    i = length(indata) + 1
    indata["col$i"] = [randstring(10) for j=1:20]

    write(f, indata; hdutype=ASCIITableHDU)

    # For ASCII tables, the types don't round trip so we need to define the
    # expected output type for each input type.
    expected_type = Dict(Int16=>Int32, Int32=>Int32,
                         Float32=>Float64, Float64=>Float64,
                         String=>String)
    colnames = FITSIO.colnames(f[3])
    for (colname, incol) in indata
        outcol = read(f[3], colname)  # table is in extension 3
        @test outcol == incol
        @test eltype(outcol) == expected_type[eltype(incol)]
        @test colname in colnames
    end
    # test show/repr on ASCIITableHDU by checking that a couple lines are what we expect
    lines = split(repr(f[3]), "\n")
    @test lines[4] == "Rows: 20"
    @test lines[6] == "         col3  Float64  E26.17  "
    close(f)
    rm(fname, force=true)
end

@testset "FITSHeader" begin
    fname = tempname() * ".fits"
    f = FITS(fname, "w")

    # test that show() works on an empty file and that the beginning and end
    # arre what we expect.
    s = repr(f)
    @test s[1:6] == "File: "
    @test s[end-7:end] == "No HDUs."

    @test_throws ErrorException FITSHeader(["KEY1"], [1, 2, 3], ["comment 1", "comment 2"])

    inhdr = FITSHeader(["FLTKEY", "INTKEY", "BOOLKEY", "STRKEY", "COMMENT",
                        "HISTORY"],
                       [1.0, 1, true, "string value", nothing, nothing],
                       ["floating point keyword",
                        "",
                        "boolean keyword",
                        "string value",
                        "this is a comment",
                        "this is a history"])

    @test repr(inhdr) == """
FLTKEY  =                  1.0 / floating point keyword
INTKEY  =                    1
BOOLKEY =                    T / boolean keyword
STRKEY  =       'string value' / string value
COMMENT this is a comment
HISTORY this is a history"""

    inhdr["INTKEY"] = 2  # test setting by key
    inhdr[1] = 2.0  # test settting by index
    set_comment!(inhdr, "INTKEY", "integer keyword") # test setting a comment

    # Test reading possibly missing keyword
    @test_throws KeyError inhdr["BADKEY"]
    @test getkey(inhdr, "BADKEY", nothing) === nothing
    @test getkey(inhdr, "INTKEY", nothing) == inhdr["INTKEY"]

    indata = reshape(Float32[1:100;], 5, 20)
    write(f, indata; header=inhdr)

    # Write a second block.
    inhdr2 = deepcopy(inhdr)
    inhdr2["INTKEY"] = 3 # Set it to a different value.
    write(f, indata; header=inhdr2)

    outhdr = read_header(f[1])
    @test outhdr["FLTKEY"] === 2.0
    @test outhdr["INTKEY"] === 2
    @test outhdr["BOOLKEY"] === true
    @test outhdr["STRKEY"] == "string value"
    @test get_comment(outhdr, 13) == "this is a comment"
    @test get_comment(outhdr, 14) == "this is a history"
    @test length(outhdr) == 14
    @test haskey(outhdr, "FLTKEY")

    # Read entire header as a single string
    s = read_header(f[1], String)
    @test s[1:9] == "SIMPLE  ="  # all headers should start with this.
    @test length(s) == (9 + length(inhdr)) * 80  # 9 lines = 8 default + "END"

    # Test to check that read_header gets the right block even after reading another.
    s_reread = read_header(f[1])
    s_reread = read_header(f[2])
    s_reread = read_header(f[1], String)
    @test s == s_reread

    # update an existing keyword, and read it directly
    write_key(f[1], "FLTKEY", 3.0)
    @test read_key(f[1], 9) == ("FLTKEY", 3.0, "floating point keyword")
    @test read_key(f[1], "FLTKEY") == (3.0, "floating point keyword")

    # Test appending a keyword, then modifying a keyword of different
    # values with write_key()
    for value in [1.0, "string value", 42, false, nothing]
        write_key(f[1], "NEWKEY", value, "new key comment")
        @test read_key(f[1], "NEWKEY") == (value, "new key comment")
        @test read_key(f[1], 15) == ("NEWKEY", value, "new key comment")
    end

    # Test that show() works and that the beginning of output is what we expect.
    @test repr(f)[1:6] == "File: "

    # Clean up from last test.
    close(f)
    rm(fname, force=true)
end

# -----------------------------------------------------------------------------
# parsing non-standard keyword records

# `create_test_file` : Create a simple FITS file for testing, with the
# given header string added after the required keywords. The length of
# `header` must be a multiple of 80.  The purpose of creating such
# files is to test the parsing of non-standard FITS keyword records
# (non-standard files can't be created with cfitsio).

function create_test_file(fname::AbstractString, header::String)
    if length(header) % 80 != 0
        error("length of header must be multiple of 80")
    end

    f = open(fname, "w")

    stdhdr = "SIMPLE  =                    T / file does conform to FITS standard             BITPIX  =                  -64 / number of bits per data pixel                  NAXIS   =                    2 / number of data axes                            NAXIS1  =                   10 / length of data axis 1                          NAXIS2  =                   10 / length of data axis 2                          EXTEND  =                    T / FITS dataset may contain extensions            "
    endline = "END                                                                             "
    data = fill(0., (10, 10))  # 10x10 array of big-endian Float64 zeros

    # write header
    write(f, stdhdr)
    write(f, header)
    write(f, endline)

    # add padding
    block_position = (length(stdhdr) + length(header) + length(endline)) % 2880
    padding = (block_position == 0) ? 0 : 2880 - block_position
    write(f, " "^padding)

    # write data
    write(f, data)

    # add padding
    block_position = sizeof(data) % 2880
    padding = (block_position == 0) ? 0 : 2880 - block_position
    write(f, fill(0x00, (padding,)))

    close(f)
end

@testset "Non-standard keywords" begin
    fname = tempname() * ".fits"
    fname2 = tempname() * ".fits"
    try
        # Create a test file with a few non-standard keyword records
        header = "        Warning: CROTA2 is inaccurate due to considerable skew                  SKEW    =  1.9511305786508E+00,  1.9234924037208E+00 /Measure of skew           "
        create_test_file(fname, header)

        # check that we can read the header (and data).
        f = FITS(fname)
        hdr = read_header(f[1])
        @test hdr["SKEW"] == "1.9511305786508E+00,"
        @test get_comment(hdr, "SKEW") == "1.9234924037208E+00 /Measure of skew"
        data = read(f[1])
        @test data == zeros(10, 10)
        close(f)

        # Test that we can read is at a memory backed file
        f = FITS(read(fname))
        hdr = read_header(f[1])
        data = read(f[1])
        @test data == zeros(10, 10)
        close(f)

        # test that we can write it back out.
        f2 = FITS(fname2, "w")
        write(f2, data; header=hdr)
        close(f2)
    finally
        rm(fname, force=true)
        rm(fname2, force=true)
    end
end

@test_deprecated FITSIO.libcfitsio_version()

# test we can still access Libcfitsio

@test begin
    using FITSIO.Libcfitsio
    Libcfitsio.libcfitsio_version() isa VersionNumber
end


using CUDAdrv

using Test

dev = CuDevice(0)
ctx = CuContext(dev)

md = CuModuleFile(joinpath(@__DIR__, "vadd.ptx"))
vadd = CuFunction(md, "kernel_vadd")

dims = (3,4)
a = round.(rand(Float32, dims) * 100)
b = round.(rand(Float32, dims) * 100)
c = similar(a)

d_a = Mem.upload(a)
d_b = Mem.upload(b)
d_c = Mem.alloc(c)

len = prod(dims)
cudacall(vadd, Tuple{CuPtr{Cfloat},CuPtr{Cfloat},CuPtr{Cfloat}}, d_a, d_b, d_c; threads=len)

Mem.download!(c, d_c)
@test a+b ≈ c

destroy!(ctx)


struct NullData end
const DEFAULT_DATA = Iterators.cycle((NullData(),))

"""
Interactive training with solution animation

"""
function vis_train(
    loss,
    _θ,
    opt,
    _data = DEFAULT_DATA;
    cb = (args...) -> false,
    maxiters = get_maxiters(data),
    progress = true,
    save_best = true,
)

    θ = copy(_θ)
    ps = Flux.params(θ)

    if _data == DEFAULT_DATA && maxiters == typemax(Int)
        error("A data iterator must be provided or the `maxiters` keyword argument must be set.")
    elseif _data == DEFAULT_DATA && maxiters != typemax(Int)
        data = Iterators.repeated((), maxiters)
    elseif maxiters != typemax(Int)
        data = Iterators.take(_data, maxiters) # an iterator that generates at most the first n elements of iter
    else
        data = _data
    end

    t0 = time()

    local x, min_err
    min_err = typemax(eltype(θ)) # dummy variables
    min_opt = 1

    anim = @animate for (i, d) in enumerate(data) # Iterator that yields (i, d) where i is a counter starting at 1,
                                                  # and d is the d-th value from dataset
        # calculate gradients of loss function
        # gradient() requires two args: f and params
        gs = Flux.Zygote.gradient(ps) do
            x = loss(θ, d...)
            first(x)
        end
        cb_call = cb(θ, x...)
        if !(typeof(cb_call) <: Bool)
            error("The callback function should return a boolean to decide whether stopping the optimization process.")
        elseif cb_call # stop
            break
        end

        DiffEqFlux.update!(opt, ps, gs)
        if save_best
            if first(x) < first(min_err) # we've found a better solution
                min_opt = opt
                min_err = x
            end
            if i == maxiters  # for last iteration, revert to best
                opt = min_opt
                cb(θ, min_err...)
            end
        end
    end

    t1 = time()

    res = Optim.MultivariateOptimizationResults(
        opt,
        _θ, # initial_x,
        θ, # pick_best_x(f_incr_pick, state),
        first(x), # pick_best_f(f_incr_pick, state, d),
        maxiters, # iteration,
        maxiters >= maxiters, # iteration == options.iterations,
        false, # x_converged,
        0.0, # T(options.x_tol),
        0.0, # T(options.x_tol),
        NaN, # x_abschange(state),
        NaN, # x_abschange(state),
        false, # f_converged,
        0.0, # T(options.f_tol),
        0.0, # T(options.f_tol),
        NaN, # f_abschange(d, state),
        NaN, # f_abschange(d, state),
        false, # g_converged,
        0.0, # T(options.g_tol),
        NaN, # g_residual(d),
        false, # f_increased,
        nothing,
        maxiters,
        maxiters,
        0,
        true,
        NaN,
        t1 - t0,
    )

    return res, anim

end


using ImageCore, Colors, FixedPointNumbers, OffsetArrays, Test

if VERSION >= v"1.2.0-DEV.229"
    sumsz(img) = Base.dims2string(size(img)) * ' '
else
    sumsz(img) = ""
end

@testset "show" begin
    thismodule = string(@__MODULE__)
    if thismodule != "Main"
        prefixF = "FixedPointNumbers."
        prefixC = "ColorTypes."
    else
        prefixF = prefixC = ""
    end
    rgb32 = rand(RGB{Float32}, 3, 5)
    v = view(rgb32, 2:3, :)
    @test summary(v) == "2×5 view(::Array{RGB{Float32},2}, 2:3, :) with eltype $(prefixC)RGB{Float32}"
    a = channelview(rgb32)
    @test summary(a) == "3×3×5 reinterpret(Float32, ::Array{RGB{Float32},3})"
    num64 = rand(3,5)
    b = colorview(RGB, num64)
    @test summary(b) == "5-element reshape(reinterpret(RGB{Float64}, ::Array{Float64,2}), 5) with eltype $(prefixC)RGB{Float64}"
    rgb8 = rand(RGB{N0f8}, 3, 5)
    c = rawview(channelview(rgb8))
    @test summary(c) == "3×3×5 rawview(reinterpret(N0f8, ::Array{RGB{N0f8},3})) with eltype UInt8"
    @test summary(rgb8) == "3×5 Array{RGB{N0f8},2} with eltype $(prefixC)RGB{$(prefixF)Normed{UInt8,8}}"
    rand8 = rand(UInt8, 3, 5)
    d = normedview(permuteddimsview(rand8, (2,1)))
    @test summary(d) == "5×3 normedview(N0f8, PermutedDimsArray(::Array{UInt8,2}, (2, 1))) with eltype $(prefixF)Normed{UInt8,8}"
    e = permuteddimsview(normedview(rand8), (2,1))
    @test summary(e) == "5×3 PermutedDimsArray(reinterpret(N0f8, ::Array{UInt8,2}), (2, 1)) with eltype $(prefixF)Normed{UInt8,8}"
    f = permuteddimsview(normedview(N0f16, rand(UInt16, 3, 5)), (2,1))
    @test summary(f) == "5×3 PermutedDimsArray(reinterpret(N0f16, ::Array{UInt16,2}), (2, 1)) with eltype $(prefixF)Normed{UInt16,16}"
    g = channelview(rgb8)
    @test summary(g) == "3×3×5 reinterpret(N0f8, ::Array{RGB{N0f8},3})"
    h = OffsetArray(rgb8, -1:1, -2:2)
    @test summary(h) == "$(sumsz(h))OffsetArray(::Array{RGB{N0f8},2}, -1:1, -2:2) with eltype $(prefixC)RGB{$(prefixF)Normed{UInt8,8}} with indices -1:1×-2:2"
    i = channelview(h)
    @test summary(i) == "$(sumsz(i))reinterpret(N0f8, OffsetArray(::Array{RGB{N0f8},3}, 1:1, -1:1, -2:2)) with indices 1:3×-1:1×-2:2"
    c = channelview(rand(RGB{N0f8}, 2))
    o = OffsetArray(c, -1:1, 0:1)
    @test summary(o) == "$(sumsz(o))OffsetArray(reinterpret(N0f8, ::Array{RGB{N0f8},2}), -1:1, 0:1) with eltype $(prefixF)Normed{UInt8,8} with indices -1:1×0:1"
    # Issue #45
    a = collect(tuple())
    @test summary(a) == "0-element Array{Union{},1}"
    b = view(a, :)
    @test summary(b) == "0-element view(::Array{Union{},1}, :) with eltype Union{}"
end

nothing


using IPIL8
using Test

@testset "IPIL8.jl" begin
    include("direct.jl");
end


module ParetoSmooth
using Requires
using DocStringExtensions

function __init__()
    @require Turing = "fce5fe82-541a-59a6-adf8-730c64b5f9a0" include("TuringHelpers.jl")
    @require MCMCChains = "c7f686f2-ff18-58e9-bc7b-31028e88f75d" include(
        "MCMCChainsHelpers.jl"
    )
end

include("AbstractCV.jl")
include("ESS.jl")
include("GPD.jl")
include("InternalHelpers.jl")
include("ImportanceSampling.jl")
include("LeaveOneOut.jl")
include("ModelComparison.jl")
include("PublicHelpers.jl")

end


const arf_rnd_t = fmpr_rnd_t

const ARF_RND_DOWN = FMPR_RND_DOWN
const ARF_RND_UP = FMPR_RND_UP
const ARF_RND_FLOOR = FMPR_RND_FLOOR
const ARF_RND_CEIL = FMPR_RND_CEIL
const ARF_RND_NEAR = FMPR_RND_NEAR

const ARF_PREC_EXACT = WORD_MAX

################################################################################
#
#  Types and memory management for arf
#
################################################################################

const arf_t = arf_struct

arf() = begin
    z = arf_struct(0, 0, 0, 0)
    ccall((:arf_init, :libarb), Void, (Ref{arf_t}, ), z)
    finalizer(z, x -> ccall((:arf_clear, :libarb), Void, (Ref{arf_t}, ), x))
    z
end

################################################################################
#
#  Conversions
#
################################################################################

convert(::Type{arf_struct}, x::Fslong)        = (y = arf(); arf_set_si(y, x); y)
convert(::Type{arf_struct}, x::Cdouble)       = (y = arf(); arf_set_d(y, x); y)
convert(::Type{arf_struct}, x::BigFloat)      = (y = arf(); arf_set_mpfr(y, x); y)
convert(::Type{arf_struct}, x::BigInt)        = (y = arf(); arf_set_mpz(y, x); y)

convert(::Type{Fslong}, x::arf_struct, rnd)   = arf_get_si(x, rnd)
convert(::Type{Cdouble}, x::arf_struct, rnd)  = arf_get_d(x, rnd)
convert(::Type{BigFloat}, x::arf_struct, rnd) = (y = BigFloat(); arf_get_mpfr(y, x, rnd); y)

################################################################################
#
#  String I/O
#
################################################################################

arf_printd(y::arf_t, d) = ccall((:arf_printd, :libarb), Void, (Ref{arf_t}, Fslong), y, d)

show(io::IO, x::arf_t) = begin
    prec = 256
    d = ceil(Int, prec * 0.30102999566398119521)
    arf_printd(x, d)
end

################################################################################
#
#  Binary operations
#
################################################################################

arf_add(z::arf_t, x::arf_t, y::arf_t, prec, rnd) = ccall((:arf_add, :libarb), Cint, (Ref{arf_t}, Ref{arf_t}, Ref{arf_t}, Fslong, arf_rnd_t), z, x, y, prec, rnd)

################################################################################
#
#  Shifting
#
################################################################################

arf_mul_2exp_si(y::arf_t, x::arf_t, e) = ccall((:arf_mul_2exp_si, :libarb), Void, (Ref{arf_t}, Ref{arf_t}, Fslong), y, x, e)

################################################################################
#
#  Unsafe setting
#
################################################################################

for (typeofy, passtoc) in ((arf_t, Ref{arf_t}), (Ptr{arf_t}, Ptr{arf_t}))
    @eval begin
        arf_set(y::($typeofy), x::arf_t)         = ccall((:arf_set,      :libarb), Void, (($passtoc), Ref{arf_t}),    y, x)
        arf_set_si(y::($typeofy), x::Fslong)     = ccall((:arf_set_si,   :libarb), Void, (($passtoc), Fslong),        y, x)
        arf_set_ui(y::($typeofy), x::Fulong)     = ccall((:arf_set_ui,   :libarb), Void, (($passtoc), Fulong),        y, x)
        arf_set_d(y::($typeofy), x::Cdouble)     = ccall((:arf_set_d,    :libarb), Void, (($passtoc), Cdouble),       y, x)
        arf_set_fmpz(y::($typeofy), x::fmpz)     = ccall((:arf_set_fmpz, :libarb), Void, (($passtoc), Ref{fmpz}),     y, x)
        arf_set_mpz(y::($typeofy), x::BigInt)    = ccall((:arf_set_mpz,  :libarb), Void, (($passtoc), Ref{BigInt}),   y, x)
        arf_set_mpfr(y::($typeofy), x::BigFloat) = ccall((:arf_set_mpfr, :libarb), Void, (($passtoc), Ref{BigFloat}), y, x)

        arf_swap(y::($typeofy), x::arf_t)        = ccall((:arf_swap,     :libarb), Void, (($passtoc), Ref{arf_t}),    y, x)
    end
end

################################################################################
#
#  Unsafe getting
#
################################################################################

arf_get_si(x::arf_t, rnd)                = ccall((:arf_get_si,   :libarb), Fslong,  (               Ref{arf_t}, Cint),    x, rnd)
arf_get_d(x::arf_t, rnd)                 = ccall((:arf_get_d,    :libarb), Cdouble, (               Ref{arf_t}, Cint),    x, rnd)
arf_get_mpfr(y::BigFloat, x::arf_t, rnd) = ccall((:arf_get_mpfr, :libarb), Void,    (Ref{BigFloat}, Ref{arf_t}, Cint), y, x, rnd)


using SOAPlite
using Test, LinearAlgebra, BenchmarkTools
using JuLIP, JuLIP.Testing

@testset "SOAPlite.jl" begin
    include("test.jl")
end


# include -
include("Include.jl")


module TestCv

using Test
using LightGBM

train_matrix = rand(1000,70) # create random dataset
train_labels = rand([0, 1], 1000)
splits = (
    collect(1:200), collect(201:400),
    collect(401:600), collect(601:800),
    collect(801:1000)
)

estimator = LightGBM.LGBMClassification(
    objective = "binary",
    num_class = 1,
    is_training_metric = true,
    metric = ["auc"]
)

@testset "test cv with X y input -- binary" begin
    # Act
    output = LightGBM.cv(estimator, train_matrix, train_labels, splits, verbosity = -1);

    # Assert
    @test output isa Dict{String,Dict{String,Vector{Float64}}}
    @test length(output["validation"]["auc"]) == 5
    @test length(output["training"]["auc"]) == 5
end

@testset "test cv with Dataset input -- binary" begin
    # Arrange
    train_dataset = LightGBM.LGBM_DatasetCreateFromMat(train_matrix, "")
    LightGBM.LGBM_DatasetSetField(train_dataset, "label", train_labels)

    # Act
    output = LightGBM.cv(estimator, train_dataset, splits, verbosity = -1);

    # Assert
    @test output isa Dict{String,Dict{String,Vector{Float64}}}
    @test length(output["validation"]["auc"]) == 5
    @test length(output["training"]["auc"]) == 5
end

end # module


"""
Generate an output column by evaluating an `Expression` against `source`.
"""
function generate_column(source::Any, e::Expression)
    if is_constant(e.body)
        if isa(e.body, Expr)
            new_column(materialize(source), e.tuple_form(()))
        else
            new_column(materialize(source), e.body)
        end
    elseif is_column(e.body)
        # TODO: Make it possible to avoid copying here.
        copy_column(materialize(source), e.body)
    else
        gen_column(
            materialize(source),
            e.tuple_form,
            e.input_columns,
        )
    end
end


"""
    loadFCSHeader(fn::String)::Tuple{Vector{Int}, Dict{String,String}}

Efficiently extract data offsets and keyword dictionary from an FCS file.
"""
function loadFCSHeader(fn::String)::Tuple{Vector{Int},Dict{String,String}}
    open(fn) do io
        offsets = FCSFiles.parse_header(io)
        params = FCSFiles.parse_text(io, offsets[1], offsets[2])
        FCSFiles.verify_text(params)
        (offsets, params)
    end
end

"""
    getFCSSize(offsets, params)::Tuple{Int,Int}

Convert the offsets and keywords from an FCS file to cell and parameter count,
respectively.
"""
function getFCSSize(offsets, params)::Tuple{Int,Int}
    nData = parse(Int, params["\$TOT"])
    nParams = parse(Int, params["\$PAR"])

    if params["\$DATATYPE"] != "F"
        @error "Only float32 FCS files are currently supported"
        error("Unsupported FCS format")
    end

    beginData = parse(Int, params["\$BEGINDATA"])
    endData = parse(Int, params["\$ENDDATA"])

    #check that the $TOT and $PAR look okay
    if !(offsets[3] == 0 && offsets[4] == 0) && (
        (
            1 + offsets[4] - offsets[3] != nData * nParams * 4 &&
            offsets[4] - offsets[3] != nData * nParams * 4
        ) ||
        offsets[3] != beginData ||
        offsets[4] != endData
    )
        @warn "Data size mismatch, FCS is likely broken."
    end

    return (nData, nParams)
end

"""
    loadFCSSizes(fns::Vector{String})

Load cell counts in many FCS files at once. Useful as input for `slicesof`.
"""
function loadFCSSizes(fns::Vector{String})::Vector{Int}
    [(
        begin
            o, s = loadFCSHeader(fn)
            getFCSSize(o, s)[1]
        end
    ) for fn in fns]
end

"""
    loadFCS(fn::String; applyCompensation::Bool=true)::Tuple{Dict{String,String}, Matrix{Float64}}

Read a FCS file. Return a tuple that contains in order:

- dictionary of the keywords contained in the file
- raw column names
- prettified and annotated column names
- raw data matrix

If `applyCompensation` is set, the function parses and retrieves a spillover
matrix (if any valid keyword in the FCS is found that would contain it) and
applies it to compensate the data.
"""
function loadFCS(
    fn::String;
    applyCompensation::Bool = true,
)::Tuple{Dict{String,String},Matrix{Float64}}
    fcs = FileIO.load(fn)
    meta = getMetaData(fcs.params)
    data = hcat(map(x -> Vector{Float64}(fcs.data[x]), meta[:, :N])...)
    if applyCompensation
        spill = getSpillover(fcs.params)
        if spill != nothing
            names, mtx = spill
            cols = indexin(names, meta[:, :N])
            if any(cols .== nothing)
                @error "Unknown columns in compensation matrix" names cols
                error("Invalid compensation matrix")
            end
            compensate!(data, mtx, Vector{Int}(cols))
        end
    end
    return (fcs.params, data)
end

"""
    loadFCSSet(name::Symbol, fns::Vector{String}, pids=workers(); applyCompensation=true, postLoad=(d,i)->d)::LoadedDataInfo

This runs the FCS loading machinery in a distributed way, so that the files
`fns` (with full path) are sliced into equal parts and saved as a distributed
variable `name` on workers specified by `pids`.

`applyCompensation` is passed to loadFCS function.

See `slicesof` for description of the slicing.

`postLoad` is applied to the loaded FCS file data (and the index) -- use this
function to e.g. filter out certain columns right on loading, using `selectFCSColumns`.

The loaded dataset can be manipulated by the distributed functions, e.g.
- `dselect` for removing columns
- `dscale` for normalization
- `dtransform_asinh` (and others) for transformation
- etc.
"""
function loadFCSSet(
    name::Symbol,
    fns::Vector{String},
    pids = workers();
    applyCompensation = true,
    postLoad = (d, i) -> d,
)::LoadedDataInfo
    slices = slicesof(loadFCSSizes(fns), length(pids))
    distributed_foreach(
        slices,
        (slice) -> Base.eval(
            Main,
            :(
                begin
                    $name = vcollectSlice(
                        (i) -> last($postLoad(
                            loadFCS($fns[i]; applyCompensation = $applyCompensation),
                            i,
                        )),
                        $slice,
                    )
                    nothing
                end
            ),
        ),
        pids,
    )
    return LoadedDataInfo(name, pids)
end

"""
    selectFCSColumns(selectColnames::Vector{String})

Return a function useful with `loadFCSSet`, which loads only the specified
(prettified) column names from the FCS files. Use `getMetaData`,
`getMarkerNames` and `cleanNames!` to retrieve the usable column names for a
FCS.
"""
function selectFCSColumns(selectColnames::Vector{String})
    ((metadata, data), idx) -> begin
        _, names = getMarkerNames(getMetaData(metadata))
        cleanNames!(names)
        colIdxs = indexin(selectColnames, names)
        if any(colIdxs .== nothing)
            @error "Some columns were not found"
            error("unknown column")
        end
        (metadata, data[:, colIdxs])
    end
end

"""
    distributeFCSFileVector(name::Symbol, fns::Vector{String}, pids=workers())::LoadedDataInfo

Distribute a vector of integers among the workers that describes which file
from `fns` the cell comes from. Useful for producing per-file statistics. The
vector is saved on workers specified by `pids` as a distributed variable
`name`.
"""
function distributeFCSFileVector(
    name::Symbol,
    fns::Vector{String},
    pids = workers(),
)::LoadedDataInfo
    sizes = loadFCSSizes(fns)
    slices = slicesof(sizes, length(pids))
    return distributeFileVector(name, sizes, slices, pids)
end

"""
    distributeFileVector(name::Symbol, sizes::Vector{Int}, slices::Vector{Tuple{Int,Int,Int,Int}}, pids=workers())::LoadedDataInfo

Generalized version of `distributeFCSFileVector` that produces the integer
vector from any `sizes` and `slices`.
"""
function distributeFileVector(
    name::Symbol,
    sizes::Vector{Int},
    slices::Vector{Tuple{Int,Int,Int,Int}},
    pids = workers(),
)::LoadedDataInfo
    distributed_foreach(
        slices,
        (slice) ->
            Base.eval(Main, :($name = collectSlice((i) -> fill(i, $sizes[i]), $slice))),
        pids,
    )
    return LoadedDataInfo(name, pids)
end

"""
    function getCSVSize(fn::String; args...)::Tuple{Int,Int}

Read the dimensions (number of rows and columns, respectively) from a CSV file
`fn`. `args` are passed to function `CSV.file`.

# Example

    getCSVSize("test.csv", header=false)
"""
function getCSVSize(fn::String; args...)::Tuple{Int,Int}
    n = 0
    k = 0
    # ideally, this will not try to load the whole CSV in the memory
    for row in CSV.File(fn, type = Float64; args...)
        n += 1
        if length(row) > k
            k = length(row)
        end
    end
    return (n, k)
end

"""
    function loadCSVSizes(fns::Vector{String}; args...)::Vector{Int}

Determine number of rows in a list of CSV files (passed as `fns`). Equivalent
to `loadFCSSizes`.
"""
function loadCSVSizes(fns::Vector{String}; args...)::Vector{Int}
    [getCSVSize(fn, type = Float64; args...)[1] for fn in fns]
end

"""
    function loadCSV(fn::String; args...)::Matrix{Float64}

CSV equivalent of `loadFCS`. The metadata (header, column names) are not
extracted. `args` are passed to `CSV.read`.
"""
function loadCSV(fn::String; args...)::Matrix{Float64}
    CSV.read(fn, DataFrame, type = Float64; args...) |> Matrix{Float64}
end

"""
    function loadCSVSet(
        name::Symbol,
        fns::Vector{String},
        pids = workers();
        postLoad = (d, i) -> d,
        csvargs...,
    )::LoadedDataInfo

CSV equivalent of `loadFCSSet`. `csvargs` are passed as keyword arguments to
CSV-loading functions.
"""
function loadCSVSet(
    name::Symbol,
    fns::Vector{String},
    pids = workers();
    postLoad = (d, i) -> d,
    csvargs...,
)::LoadedDataInfo
    slices = slicesof(loadCSVSizes(fns; csvargs...), length(pids))
    distributed_foreach(
        slices,
        (slice) -> Base.eval(
            Main,
            :(
                begin
                    $name = vcollectSlice(
                        (i) -> $postLoad(loadCSV($fns[i]; $csvargs...), i),
                        $slice,
                    )
                    nothing
                end
            ),
        ),
        pids,
    )
    return LoadedDataInfo(name, pids)
end


#=

utils.jl
========
    A collection of useful methods that are used in the module.

    Methods:
    -------

Author: Fady Shoukry
Date: 01/30/2016
=#
function createSymsBlock{T<:Any}(modelname::Symbol, syms_dict::Dict{Symbol}{T})
    """
    Return a dynamically generated block of code that, when evaluated, will
    define variables for each key in syms_dict
    """
    blk = Expr(:block)
    for (key,val) in syms_dict
        push!(blk.args, :($key = $val))
    end
    blk
end

function createConstraintsBlock(modelname::Symbol, constraints::Array{Expr})
    """
    Return a dynamically generated block of code that, when evaluated, will add
    all the constraints to the model required
    """
    blk = Expr(:block)
    # First replace all symbols
    for expr in constraints
        str = string(expr)
        push!(blk.args, quote
                            try
                                JuMP.@constraint($modelname, $expr)
                            catch e
                                JuMP.@NLconstraint($modelname, $expr)
                                warn($str, " is a non-linear constraint")
                            end
                        end)
    end
    blk
end

function createVarsBlock(modelname::Symbol, vars::Vector)
    """
    Return a dynamically generated block of code that, when evaluated, will add
    all the variables to the model required
    """
    blk = Expr(:block)
    # First replace all symbols
    for var in vars
        push!(blk.args, :(JuMP.@variable($modelname, $var)))
    end
    blk
end

function createJuMPModelGenFunc{T<:Any}(vars::Vector, sense::Symbol,
    objective::Expr, constraints::Array{Expr}, syms_dict::Dict{Symbol}{T})
    """
    Dynamically generate and return an anonymous function that creates
    and returns the final JuMP model
    """
    model_sym = :model
    syms_blk = createSymsBlock(model_sym, syms_dict)
    objective_ex = :(JuMP.@objective($model_sym, $sense, $objective))
    vars_blk = createVarsBlock(model_sym, vars)
    constraints_blk = createConstraintsBlock(model_sym, constraints)
    @eval function (solver)
        $model_sym = JuMP.Model(solver = solver)
        $syms_blk
        $vars_blk
        $objective_ex
        $constraints_blk
        $model_sym
    end
end


#
# Dimension: translates sequences of Symbols, Strings, or Ints to
# ordinal values, so the Dimension keys can be used for lookup in
# Arrays in Mimi. A Dimension can be declared one of several ways:
# dim = Dimension(:foo, :bar, :baz)      # varargs
# dim = Dimension([:foo, :bar, :baz])    # Vector
# dim = Dimension(2010:2100)             # Range
# dim = Dimension(4)                     # Same as 1:4
#
# Similarly, values can be referenced several ways:
# value  = Dimension[:foo]               # single lookup
# values = Dimension[(:foo, :bar)]       # Tuple of values
# values = Dimension[[2010, 2020, 2030]] # Vector of values
# values = Dimension[2010, 2020, 2030]   # Varargs
# values = Dimension[2010:10:2030]       # AbstractRange
#
using DataStructures

key_type(dim::Dimension{T}) where {T <: DimensionKeyTypes} = T

#
# Iteration and basic dictionary methods are delegated to the internal dict
#
Base.length(dim::Dimension) = length(dim.dict)
Base.iterate(dim::Dimension, state...) = iterate(dim.dict, state...)

Base.keys(dim::Dimension)   = keys(dim.dict)
Base.values(dim::Dimension) = values(dim.dict)

Base.get(dim::Dimension, key::Union{Number, Symbol, String}, default::Any) = get(dim.dict, key, default)

Base.getindex(dim::Dimension, key::Colon) = collect(values(dim.dict))

Base.getindex(dim::AbstractDimension, key::Union{Number, Symbol, String}) = getindex(dim.dict, key)

# Support dim[[:foo, :bar, :baz]], dim[(:foo, :bar, :baz)], and dim[2010:2020]
Base.getindex(dim::AbstractDimension, keys::Union{Vector{T} where T, Tuple, AbstractRange}) = [getindex(dim.dict, key) for key in keys]

Base.getindex(dim::AbstractDimension, keys...) = getindex(dim, keys)

Base.length(dim::RangeDimension) = length(dim.range)
Base.iterate(dim::RangeDimension, state...)  = iterate(dim.range, state...)

Base.keys(dim::RangeDimension)   = collect(dim.range)
Base.values(dim::RangeDimension) = collect(1:length(dim.range))

# Get last value from OrderedDict of keys
Base.lastindex(dim::AbstractDimension) = dim.dict.keys[length(dim)]

#
# Compute the index of a "key" (e.g., a year) in the range.
#
function Base.get(dim::RangeDimension, key::Int, default::Any=0)
    r = dim.range
    i = key - r.start
    return i == 0 ? 1 : (i % r.step != 0 ? default : 1 + div(i, r.step))
end

# Support dim[[2010, 2020, 2030]], dim[(:foo, :bar, :baz)], and dim[2010:2050]
Base.getindex(dim::RangeDimension, keys::Union{Vector{Int}, Tuple, AbstractRange}) = [get(dim, key, 0) for key in keys]


using OpenBLAS_jll, OpenBLAS32_jll, MKL_jll, CompilerSupportLibraries_jll
using Pkg, Artifacts, Base.BinaryPlatforms, Libdl, Test

include("utils.jl")

# Compile `dgemm_test.c` and `sgesv_test.c` against the given BLAS/LAPACK
function run_test((test_name, test_expected_outputs, test_success), libblas_name, libdirs, interface, backing_libs)
    # We need to configure this C build a bit
    cflags = String[
        "-g",
    ]
    if interface == :ILP64
        push!(cflags, "-DILP64")
    end

   # If our GCC is 64-bit but Julia is 32-bit, pass -m32
   if needs_m32()
       push!(cflags, "-m32")
   end

    ldflags = String[
        # Teach it to find that libblas and its dependencies at build time
        ("-L$(pathesc(libdir))" for libdir in libdirs)...,
        "-l$(libblas_name)",
    ]

    if !Sys.iswindows()
        # Teach it to find that libblas and its dependencies at run time
        append!(ldflags, ("-Wl,-rpath,$(pathesc(libdir))" for libdir in libdirs))
    end

    mktempdir() do dir
        @info("Compiling `$(test_name)` against $(libblas_name) ($(backing_libs)) in $(dir)")
        srcdir = joinpath(@__DIR__, test_name)
        make_cmd = `$(make) -sC $(pathesc(srcdir)) prefix=$(pathesc(dir)) CFLAGS="$(join(cflags, " "))" LDFLAGS="$(join(ldflags, " "))"`
        p = run(ignorestatus(make_cmd))
        if !success(p)
            @error("compilation failed", srcdir, prefix=dir, cflags=join(cflags, " "), ldflags=join(ldflags, " "))
        end
        @test success(p)

        env = Dict(
            # We need to tell it how to find CSL at run-time
            LIBPATH_env => append_libpath(libdirs),
            "LBT_DEFAULT_LIBS" => backing_libs,
            "LBT_STRICT" => 1,
            "LBT_VERBOSE" => 1,
        )
        cmd = `$(dir)/$(test_name)`
        p, output = capture_output(addenv(cmd, env))

        expected_return_value = success(p) ^ test_success
        if !expected_return_value
            @error("Test failed", env)
            println(output)
        end
        @test expected_return_value

        # Test to make sure the test ran properly
        has_expected_output = all(occursin(expected, output) for expected in test_expected_outputs)
        if !has_expected_output
            # Uh-oh, we didn't get what we expected.  Time to debug!
            @error("Test failed, got output:")
            println(output)

            # If we're not on CI, launch `gdb`
            if isempty(get(ENV, "CI", ""))
                debugger = Sys.isbsd() ? "lldb" : "gdb"
                @warn("Launching $debugger")
                cmd = `$(debugger) $(cmd)`
                run(addenv(cmd, env))
            end
        end
        @test has_expected_output
    end
end

# our tests, written in C, defined in subdirectories in `test/`
dgemm =         ("dgemm_test", ("||C||^2 is:  24.3384",),                  true)
sgesv =         ("sgesv_test", ("||b||^2 is:   3.0000",),                  true)
sgesv_failure = ("sgesv_test", ("Error: no BLAS/LAPACK library loaded!",), false)
sdot  =         ("sdot_test",  ("C is:   1.9900",),                        true)
zdotc =         ("zdotc_test", (
                     "C (cblas) is:   (  1.4700,   3.8300)",
                     "C (fortran) is: (  1.4700,   3.8300)",
                ),      true)

# Helper function to run all the tests with the given arguments
function run_all_tests(args...; tests = [dgemm, sgesv, sdot, zdotc])
    for test in tests
        run_test(test, args...)
    end
end

# Build version that links against vanilla OpenBLAS
openblas_interface = :LP64
if Sys.WORD_SIZE == 64 && Sys.ARCH != :aarch64
    openblas_interface = :ILP64
end
openblas_jll_libname = splitext(basename(OpenBLAS_jll.libopenblas_path)[4:end])[1]
@testset "Vanilla OpenBLAS_jll ($(openblas_interface))" begin
    run_all_tests(openblas_jll_libname, OpenBLAS_jll.LIBPATH_list, openblas_interface, "")
end

# Build version that links against vanilla OpenBLAS32
@testset "Vanilla OpenBLAS32_jll (LP64)" begin
    # Reverse OpenBLAS32_jll's LIBPATH_list so that we get the right openblas.so
    run_all_tests("openblas", reverse(OpenBLAS32_jll.LIBPATH_list), :LP64, "")
end

# Next, build a version that links against `libblastrampoline`, and tell
# the trampoline to forwards calls to `OpenBLAS_jll`
lbt_link_name, lbt_dir = build_libblastrampoline()
lbt_dir = joinpath(lbt_dir, binlib)

@testset "LBT -> OpenBLAS_jll ($(openblas_interface))" begin
    libdirs = unique(vcat(lbt_dir, OpenBLAS_jll.LIBPATH_list..., CompilerSupportLibraries_jll.LIBPATH_list...))
    run_all_tests("blastrampoline", libdirs, openblas_interface, OpenBLAS_jll.libopenblas_path)
end

# And again, but this time with OpenBLAS32_jll
@testset "LBT -> OpenBLAS32_jll (LP64)" begin
    libdirs = unique(vcat(lbt_dir, OpenBLAS32_jll.LIBPATH_list..., CompilerSupportLibraries_jll.LIBPATH_list...))
    run_all_tests("blastrampoline", libdirs, :LP64, OpenBLAS32_jll.libopenblas_path)
end

# Test against MKL_jll using `libmkl_rt`, which is :LP64 by default
if MKL_jll.is_available()
    @testset "LBT -> MKL_jll (LP64)" begin
        libdirs = unique(vcat(lbt_dir, MKL_jll.LIBPATH_list..., CompilerSupportLibraries_jll.LIBPATH_list...))
        run_all_tests("blastrampoline", libdirs, :LP64, MKL_jll.libmkl_rt_path)
    end

    # Test that we can set MKL's interface via an environment variable to select ILP64, and LBT detects it properly
    if Sys.WORD_SIZE == 64
        @testset "LBT -> MKL_jll (ILP64, via env)" begin
            withenv("MKL_INTERFACE_LAYER" => "ILP64") do
                libdirs = unique(vcat(lbt_dir, MKL_jll.LIBPATH_list..., CompilerSupportLibraries_jll.LIBPATH_list...))
                run_all_tests("blastrampoline", libdirs, :ILP64, MKL_jll.libmkl_rt_path)
            end
        end
    end
end

# Do we have Accelerate available?
veclib_blas_path = "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/libBLAS.dylib"
if isfile(veclib_blas_path)
    # Test that we can run BLAS-only tests without LAPACK loaded (`sgesv` test requires LAPACK symbols)
    @testset "LBT -> vecLib/libBLAS" begin
        run_all_tests("blastrampoline", [lbt_dir], :LP64, veclib_blas_path; tests=[dgemm, sdot, zdotc])
    end

    # With LAPACK as well, run all tests
    veclib_lapack_path = "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/libLAPACK.dylib"
    @testset "LBT -> vecLib/libLAPACK" begin
        run_all_tests("blastrampoline", [lbt_dir], :LP64, string(veclib_blas_path, ";", veclib_lapack_path))
    end
end

# Do we have a `blas64.so` somewhere?  If so, test with that for fun
blas64 = dlopen("libblas64", throw_error=false)
if blas64 !== nothing
    # Test that we can run BLAS-only tests without LAPACK loaded (`sgesv` test requires LAPACK symbols, blas64 doesn't have CBLAS)
    @testset "LBT -> libblas64 (ILP64, BLAS)" begin
        run_all_tests("blastrampoline", [lbt_dir], :ILP64, dlpath(blas64); tests=[dgemm, sdot])
    end

    # Check if we have a `liblapack` and if we do, run again, this time including `sgesv`
    lapack = dlopen("liblapack64", throw_error=false)
    if lapack !== nothing
        @testset "LBT -> libblas64 + liblapack64 (ILP64, BLAS+LAPACK)" begin
            run_all_tests("blastrampoline", [lbt_dir], :ILP64, "$(dlpath(blas64));$(dlpath(lapack))"; tests=[dgemm, sdot, sgesv])
        end
    end
end

# Finally the super-crazy test: build a binary that links against BOTH sets of symbols!
if openblas_interface == :ILP64
    inconsolable = ("inconsolable_test", ("||C||^2 is:  24.3384", "||b||^2 is:   3.0000"), true)
    @testset "LBT -> OpenBLAS 32 + 64 (LP64 + ILP64)" begin
        libdirs = unique(vcat(OpenBLAS32_jll.LIBPATH_list..., OpenBLAS_jll.LIBPATH_list..., CompilerSupportLibraries_jll.LIBPATH_list..., lbt_dir))
        run_test(inconsolable, lbt_link_name, libdirs, :wild_sobbing, "$(OpenBLAS32_jll.libopenblas_path);$(OpenBLAS_jll.libopenblas_path)")
    end
end

# Run our "direct" tests within Julia
include("direct.jl")


export AdamsMoulton

struct AdamsMoulton <: AbstractStateContractorName
    steps::Int
end

mutable struct AdamsMoultonFunctor{S} <: AbstractStateContractor
    f!
    Jx!
    Jp!
    nx
    np
    method_step::Int64
    η::Interval{Float64}
    μX::Vector{S}
    ρP::Vector{S}
    Dk::Vector{S}
    Jxsto::FixedCircularBuffer{Matrix{S}}
    Jpsto::FixedCircularBuffer{Matrix{S}}
    Jxsum::Vector{S}
    Jpsum::Matrix{S}
    Jxvec::Vector{S}
    Jpvec::Vector{S}
    fval::FixedCircularBuffer{Vector{Float64}}
    fk_apriori::FixedCircularBuffer{Vector{S}}
    Rk::Vector{S}
    Y0::Matrix{Float64}
    Y::Matrix{Float64}
    Jxmid_sto::Matrix{S}
    precond::LinearAlgebra.LU{Float64,Array{Float64,2}}
    JxAff::Matrix{S}
    YJxAff::Matrix{S}
    YJxΔx::Vector{S}
    Xj_delta::Vector{S}
    Ysumx::Vector{S}
    YsumP::Matrix{S}
    YJpΔp::Vector{S}
    coeffs::Vector{Float64}
    Δⱼ₊₁::Vector{S}
    is_adaptive::Bool
    γ::Float64
    lohners_start::LohnersFunctor
    A_Q::FixedCircularBuffer{Matrix{Float64}}
    A_inv::FixedCircularBuffer{Matrix{Float64}}
    Δ::FixedCircularBuffer{Vector{S}}
    X::FixedCircularBuffer{Vector{S}}
    xval::FixedCircularBuffer{Vector{Float64}}
end

function AdamsMoultonFunctor(f::F, Jx!::JX, Jp!::JP, nx::Int, np::Int, s::S,
                             t::T, steps::Int, lohners_start) where {F,JX,JP,S,T}
    η = Interval{T}(0.0,1.0)
    μX = zeros(S, nx)
    ρP = zeros(S, np)

    method_step = steps
    Dk = zeros(S, nx)

    lu_mat = zeros(nx, nx)
    for i = 1:nx
        lu_mat[i,i] = 1.0
    end
    precond = lu(lu_mat)
    Y0 = zeros(nx, nx)
    Y = zeros(nx, nx)
    Jxmid_sto = zeros(S, nx, nx)
    YJxAff = zeros(S, nx, nx)
    JxAff = zeros(S, nx, nx)
    Xj_delta = zeros(S, nx)
    YJxΔx = zeros(S, nx)
    Ysumx = zeros(S, nx)
    YsumP = zeros(S, nx, nx)
    YJpΔp = zeros(S, nx)

    Jxsto = FixedCircularBuffer{Matrix{S}}(method_step)
    Jpsto = FixedCircularBuffer{Matrix{S}}(method_step)
    for i = 1:method_step
        push!(Jxsto, zeros(S, nx, nx))
        push!(Jpsto, zeros(S, nx, np))
    end
    Jxsum = zeros(S, nx)
    Jpsum = zeros(S, nx, nx)
    Jxvec = zeros(S, nx)
    Jpvec = zeros(S, nx)

    fval = FixedCircularBuffer{Vector{Float64}}(method_step)
    fk_apriori = FixedCircularBuffer{Vector{S}}(method_step)
    A_Q = FixedCircularBuffer{Matrix{Float64}}(method_step)
    A_inv = FixedCircularBuffer{Matrix{Float64}}(method_step)
    Δ = FixedCircularBuffer{Vector{S}}(method_step)
    X = FixedCircularBuffer{Vector{S}}(method_step)
    xval = FixedCircularBuffer{Vector{Float64}}(method_step)
    for i = 1:method_step
        push!(xval, zeros(Float64, nx))
        push!(fval, zeros(Float64, nx))
        push!(fk_apriori, zeros(S, nx))
        push!(A_Q, Float64.(Matrix(I, nx, nx)))
        push!(A_inv, Float64.(Matrix(I, nx, nx)))
        push!(X, zeros(S, nx))
        push!(Δ, zeros(S, nx))
    end

    Rk = zeros(S, nx)
    coeffs = zeros(Float64, method_step + 1)
    Δⱼ₊₁ = zeros(S, nx)

    is_adaptive = false
    γ = 0.0

    AdamsMoultonFunctor{S}(f, Jx!, Jp!, nx, np, method_step, η, μX, ρP,
                           Dk, Jxsto, Jpsto, Jxsum, Jpsum, Jxvec, Jpvec, fval,
                           fk_apriori, Rk, Y0, Y, Jxmid_sto, precond, JxAff, YJxAff,
                           YJxΔx, Xj_delta, Ysumx, YsumP, YJpΔp, coeffs, Δⱼ₊₁,
                           is_adaptive, γ, lohners_start, A_Q, A_inv,
                           Δ, X, xval)
end

function compute_coefficients!(d::AdamsMoultonFunctor{S}, h::Float64, t::Float64, s::Int) where S
    if !d.is_adaptive
        if s == 1
            d.coeffs[1] = 1.0
            d.coeffs[2] = 0.5
        elseif s == 2
            d.coeffs[1] = 0.5
            d.coeffs[2] = 0.5
            d.coeffs[3] = -1.0/12.0
        elseif s == 3
            d.coeffs[1] = 5.0/12.0
            d.coeffs[2] = 8.0/12.0
            d.coeffs[3] = -1.0/12.0
            d.coeffs[4] = -1.0/24.0
        elseif s == 4
            d.coeffs[1] = 9.0/24.0
            d.coeffs[2] = 19.0/24.0
            d.coeffs[3] = -5.0/24.0
            d.coeffs[4] = 1.0/24.0
            d.coeffs[5] = -19.0/720.0
        else
            #compute_fixed_higher_coeffs!(d, s)
        end
    else
        #compute_adaptive_coeffs!(d, h, t, s)
    end

    d.γ = d.coeffs[s + 1]

    return nothing
end

function compute_Rk!(d::AdamsMoultonFunctor{T}, contract::ContractorStorage{T},
                     h::Float64, s::Int) where T<:Number

    #println("------------")
    #println("-compute_Rk-")
    #println("------------")

    coeff = d.γ*h^(d.method_step + 1)
    cycle_copyto!(d.fk_apriori, contract.fk_apriori, contract.step_count - 1)

    @__dot__ d.Rk = d.fk_apriori[1]
    #println("d.Rk[1] = $(d.Rk)")
    for i = 2:s
        @__dot__ d.Rk = d.Rk ∪ d.fk_apriori[i]
        #println("d.fk_apriori[$i] = $(d.fk_apriori[i])")
        #println(" d.Rk[$i] = $(d.Rk)")
    end
    #@show d.γ
    #@show h^(d.method_step + 1)
    @__dot__ d.Rk *= coeff

    return nothing
end

# TODO: Check block
function compute_real_sum!(d::AdamsMoultonFunctor{T}, contract::ContractorStorage{T},
                           result::StepResult{T}, h::Float64, t::Float64,
                           s::Int) where T<:Number

    old_xval = mid.(contract.Xj_0)
    cycle_eval!(d.f!, d.fval, old_xval, contract.pval, t)
    @__dot__ d.Dk = old_xval
    #@show old_xval
    for i = 1:s
        @__dot__ d.Dk += h*d.coeffs[i]*d.fval[i]
        #println("$i-th coeffs = $(d.coeffs[i]), fvals = $(d.fval[i])")
    end
    return nothing
end

function compute_jacobian_sum!(d::AdamsMoultonFunctor{T},
                           contract::ContractorStorage{T},
                           h::Float64, t::Float64, s::Int) where T<:Number

    μ!(d.μX, contract.Xj_0, contract.xval, d.η)
    ρ!(d.ρP, contract.P, contract.pval, d.η)
    cycle_eval!(d.Jx!, d.Jxsto, d.μX, d.ρP, t)
    cycle_eval!(d.Jp!, d.Jpsto, d.μX, d.ρP, t)

    #println("------------")
    #println("-compute_jacobian_sum!-")
    #println("------------")
    #@show d.Jxsto
    #@show d.Jpsto

    @__dot__ d.JxAff = d.Jxsto[1]

    #@show d.JxAff

    # contract.Xj_apriori GOOD
    # d.Xj_delta GOOD
    @__dot__ d.Xj_delta = contract.Xj_apriori - mid(contract.Xj_apriori)

    # TODO: CHECK RIGHT DELTAS...
    d.Jxsum = (h*d.coeffs[1])*d.Jxsto[1]*d.Xj_delta
    @show h, d.coeffs[1], d.Jxsto[1], d.Xj_delta
    if s > 1
        d.Jxsum += ((d.η*I + h*d.coeffs[2]*d.Jxsto[2])*contract.A_Q[1])*contract.Δ[1]
        @show h, d.coeffs[2], d.Jxsto[2], contract.A_Q[1], contract.Δ[1]
    end
    for i = 3:s
        d.Jxsum += (h*d.coeffs[i])*(d.Jxsto[i]*contract.A_Q[i])*contract.Δ[i]
        #@show h, d.coeffs[i], d.Jxsto[i], contract.A_Q[i], contract.Δ[i]
    end

    # JpSum is correct
    @__dot__ d.Jpsum = h*d.coeffs[1]*d.Jpsto[1]
    for i = 2:s
        @__dot__ d.Jpsum += h*d.coeffs[i]*d.Jpsto[i]
    end

    return nothing
end

function compute_X!(d::AdamsMoultonFunctor{T}, contract::ContractorStorage{S}) where {S, T<:Number}

    #println("------------")
    #println("-compute_X!-")
    #println("------------")

    #mul!(d.Jxvec, d.JxAff, d.Xj_delta)
    mul!(d.Jpvec, d.Jpsum, contract.rP)

    #@show d.Jxvec
    @show d.Jxsum
    # d.Jpvec is correct...
    @show d.Jpvec
    @show d.Dk
    @show d.Rk
    #@__dot__ contract.X_computed = d.Jxvec + d.Jxsum + d.Jpvec + d.Dk + d.Rk
    @__dot__ contract.X_computed = d.Jxsum + d.Jpvec + d.Dk + d.Rk
    @show contract.X_computed
    @show contract.Xj_apriori
    @__dot__ contract.X_computed = contract.X_computed ∩ contract.Xj_apriori
    @show contract.X_computed
    return nothing
end

function compute_xval!(d::AdamsMoultonFunctor{T}, contract::ContractorStorage{S}, t) where {S, T<:Number}
    @__dot__ contract.xval_computed = mid(contract.X_computed)
    @show contract.xval_computed
    d.f!(d.fval[1], contract.xval_computed, contract.pval, t)
    return nothing
end

function compute_Ainv!(d::AdamsMoultonFunctor{T}, contract::ContractorStorage{S}, s::Int) where {S, T<:Number}
    if s > 1
        mul!(d.Jxmid_sto, d.Jxsto[1], contract.A_Q[2])
        @__dot__ contract.B = mid(d.Jxmid_sto)
        calculateQ!(contract.A_Q[1], contract.B, d.nx)
        calculateQinv!(contract.A_inv[1], contract.A_Q[1], d.nx)
    end
    return nothing
end

function update_delta!(d::AdamsMoultonFunctor{T}, contract::ContractorStorage{S}, s::Int) where {S, T<:Number}
    println("update delta for s = $s order")
    if s > 0
        println("ran delta update...")
        @show d.Y0
        @show contract.A_Q[2]
        d.Y = I - d.Y0*contract.A_Q[2]
        @show d.Y
        d.precond = lu!(d.Y)
        d.YJxAff = d.precond\d.JxAff
        @show d.YJxAff
        mul!(d.YJxΔx, d.YJxAff, d.Xj_delta)
        @show d.YJxΔx
        d.Ysumx = d.precond\d.Jxsum
        d.YsumP = d.precond\d.Jpsum
        @show d.Ysumx
        @show d.YsumP
        mul!(d.YJpΔp, d.YsumP, contract.rP)
        @show d.YJpΔp
        @__dot__ d.Δⱼ₊₁ = d.YJxΔx + d.Ysumx + d.YJpΔp
        @show d.Δⱼ₊₁
    end
    return nothing
end

function store_starting_buffer!(d::AdamsMoultonFunctor{T},
                                contract::ContractorStorage{T},
                                result::StepResult{T}, t::Float64) where T

    count = contract.step_count - 1
    cycle_copyto!(d.X, result.Xⱼ, count)
    cycle_copyto!(d.xval, result.xⱼ, count)
    cycle_copyto!(d.fk_apriori, contract.fk_apriori, count)
    cycle_copyto!(d.Δ, result.Δ[1], count)
    cycle_copyto!(d.A_Q, result.A_Q[1], count)
    cycle_copyto!(d.A_inv, result.A_inv[1], count)

    # update Jacobian storage
    μ!(d.μX, result.Xⱼ, result.xⱼ, d.η)
    ρ!(d.ρP, contract.P, contract.pval, d.η)

    cycle_eval!(d.Jx!, d.Jxsto, d.μX, d.ρP, t)
    cycle_eval!(d.Jp!, d.Jpsto, d.μX, d.ρP, t)
    cycle_eval!(d.f!, d.fval, result.xⱼ, contract.pval, t)

    return nothing
end

function (d::AdamsMoultonFunctor{T})(contract::ContractorStorage{S},
                                  result::StepResult{S},
                                  count::Int) where {S, T<:Number}

    println("\n BEGIN MAIN - step: $(contract.step_count-1), method step: $(d.method_step) \n")

    t = contract.times[1]
    s = min(contract.step_count-1, d.method_step)
    #@show s
    #@show d.method_step
    if s < d.method_step - 1
        #println("---- LOHNERS STEP -----")
        d.lohners_start(contract, result, count)
        (count == 0) && store_starting_buffer!(d, contract, result, t)
        return nothing
    end

    #println("---- PILMS STEP -----")
    h = contract.hj_computed
    #@show contract.hj_computed
    compute_coefficients!(d, h, t, s)
    compute_Rk!(d, contract, h, s)
    compute_real_sum!(d, contract, result, h, t, s)
    compute_jacobian_sum!(d, contract, h, t, s)
    compute_X!(d, contract)
    compute_xval!(d, contract, t)
    compute_Ainv!(d, contract, s)
    update_delta!(d, contract, s)
    return nothing
end

function get_Δ(d::AdamsMoultonFunctor)
    if true
        return copy(get_Δ(d.lohners_start))
    end
    return copy(d.Δⱼ₊₁)
end
function advance_contractor_buffer!(d::AdamsMoultonFunctor)
    return nothing
end

function state_contractor(m::AdamsMoulton, f, Jx!, Jp!, nx, np, style, s, h)
    lohners_functor = state_contractor(LohnerContractor{m.steps}(), f, Jx!, Jp!, nx, np, style, s, h)
    AdamsMoultonFunctor(f, Jx!, Jp!, nx, np, style, s, m.steps, lohners_functor)
end
state_contractor_k(m::AdamsMoulton) = m.steps + 1
state_contractor_γ(m::AdamsMoulton) = 0.0
state_contractor_steps(m::AdamsMoulton) = m.steps
state_contractor_integrator(m::AdamsMoulton) = CVODE_Adams()


##################################################################################
# This file is part of StateSpaceEcon.jl
# BSD 3-Clause License
# Copyright (c) 2020, Bank of Canada
# All rights reserved.
##################################################################################

# """
#     AbstractSolverData

# An abstract type for the internal data structure of a dynamic solver.

# ### Implementation (for developers)

# To implement a derived class, one must specialize the following methods. Note
# that in the signatures of most of these methods, the `sd` argument of the new
# type is the last argument..

#   * Constructor - normally it would take a model and a plan together with a
#     final condition type.
#   * `global_RJ(x, exog, sd)` - compute the residual and the Jacobian (return a
#     2-tuple) of the system at the given point `x` and exogenous data `exog`.
#   * `global_R!(res, x, exog, sd)` (optional) - compute the residual in place.
#   * `assign_exog_data!(x, exog, sd)` - this function applies the exogenous
#     constraints as per the plan and the given exogenous data.
#   * `assign_final_condition!(x, exog, sd, ::Val{T})` where T is one of the
#     FCType constants. This method applies the given type of final condition. If
#     T is fcgiven, the values of the final conditions are taken from exog.
#   * `assign_update_step(x, lambda, dx, sd)` - this method must implement x = x +
#     lambda * dx
#   * `update_plan!(sd, model, plan)` - update the solver data to reflect the given
#     simulation plan.

# """
# abstract type AbstractSolverData end

# """
#     global_RJ(x::Array, exog::Array, sd::AbstractSolverData)

# Compute the residual and the Jacobian of the dynamic system of equations at the
# given point `x` using the given exogenous data `exog`. Returns a tuple of two
# elements. The first is a vector of residuals and the second is the Jacobian
# matrix.

# """
# global_RJ(point::AbstractArray{Float64}, exog_data::AbstractArray{Float64}, g::AbstractSolverData) = error("Not implemented for $(typeof(g)).")
# # export global_RJ

# """
#     global_R!(res::Vector, x::Array, exog::Array, sd::AbstractSolverData)

# Compute the residual of the dynamic system of equations at the given point `x` using the given exogenous data `exog`.
# The residual is updated in place.
# """
# global_R!(res::AbstractArray{Float64}, point::AbstractArray{Float64}, exog_data::AbstractArray{Float64}, g::AbstractSolverData) = error("Not implemented for $(typeof(g)).")
# # export global_R!

# """
#     assign_exog_data(x::Array, exog::Array, sd::AbstractSolverData)

# Apply the exogenous constraints according to the simulation plan (passed to the
# constructor of sd) with exogenous values taken from `exog`.

# """
# assign_exog_data!(x::AbstractArray{Float64,2}, exog::AbstractArray{Float64,2}, g::AbstractSolverData) = error("Not implemented for $(typeof(g)).")
# # export assign_exog_data!

# """
#     assign_final_condition!(x::Array, exog::Array, sd::AbstractSolverData, ::Val{T})

# Apply final conditions of the given type. The type parameter `T` must be one of
# the constants of type `FCType`. If `T` is fcgiven, the final condition values
# are taken from `exog`. If `T` is one of `fclevel` or `fcslope`, then the final
# conditions are taken from the steady state solution in the model instance passed
# to the constructor of `sd`.

# """
# assign_final_condition!(x::AbstractArray{Float64,2}, exog::AbstractArray{Float64,2}, g::AbstractSolverData, ::Val{T}) where T = error("Not implemented for $(typeof(g)) with final condition $(T).")
# # export assign_final_condition!

# """
#     assign_update_step!(x::Array, lambda::Real, dx::Array, sd::AbstractSolverData)

# Assign the iterative update. Equivalent to `x = x + lambda * dx`.
# """
# function assign_update_step! end
# @inline assign_update_step!(x::AbstractArray{Float64}, lambda::Float64, Δx::AbstractArray{Float64}, ::AbstractSolverData) = BLAS.axpy!(lambda, Δx, x)  # axpy!(a,x,y) <=> y .= a .* x .+ y
# # export assign_update_step!

# """
#     update_plan!(sd, model, plan)

# Update the solver data structure for the given simulation plan. The `model` must
# be the same instance that was used to create `sd`. Typically, the plan should
# span the same range as the one used when `sd` was created, although it might
# have different exogenous/endogenous assignments.

# """
# update_plan!(sd::AbstractSolverData, m::Model, p::Plan) = error("Not implemented for $(typeof(sd)).")
# # export update_plan!