using NCTiles, NCDatasets, MeshArrays, Dates

"""
    testfile(fname,checkfld)

Perform various checks on a netcdf file (fname) variable (checkfld).
"""
function testfile(fname,checkfld)
    pass = true
    ds = Dataset(fname)
    dsvar = ds[checkfld.name]
    varcheck = Dict()

    #1. checks on dimensions
    varcheck["dims"] = Dict()
    for d in checkfld.dims
        fildim = ds[d.name]
        dimcheck = Dict()
        # check size
        dimcheck["size"] = length(fildim) == length(d.values)
        if isa(fildim[1],DateTime)
            dimvals = [Hour.(ds["tim"][t] .- DateTime(replace(d.units,"days since "=>""),"yyyy-mm-dd HH:MM:SS")).value for t in 1:length(ds["tim"])]/24
            dimcheck["values"] = d.values == dimvals
        else
            dimcheck["values"] = d.values == fildim
            if length(size(d.values)) > 1
                dimcheck["values"] = d.values[:,1] == fildim
            end
        end
        pass = pass && dimcheck["size"] && dimcheck["values"]
        # check attributes
        for k in keys(fildim.attrib)
            if k == "units"
                dimcheck["units"] = fildim.attrib[k] == d.units
                pass = pass && dimcheck["units"]
            else
                dimcheck[k] = fildim.attrib[k] == d.atts[k]
                pass = pass && dimcheck[k]
            end
        end

        varcheck["dims"][d.name] = dimcheck
    end

    #2. checks on units
    for k in keys(dsvar.attrib)
        if k == "units"
            varcheck["units"] = dsvar.attrib[k] == checkfld.units
            pass = pass && varcheck["units"]
        else
            varcheck[k] = dsvar.attrib[k] == checkfld.atts[k]
            pass = pass && varcheck[k]
        end
    end

    #3. checks on values
    varcheck["values"] = false
    if isa(checkfld.values,Array)
        varcheck["values"] = true
        if isa(checkfld.values[1],Array)
            if NCTiles.hastimedim(checkfld)
                for t in 1:length(checkfld.values)
                    if length(size(dsvar)) == 3
                        varcheck["values"] = varcheck["values"] && checkfld.values[t] == dsvar[:,:,t]
                    elseif length(size(dsvar)) == 4
                        varcheck["values"] = varcheck["values"] && checkfld.values[t] == dsvar[:,:,:,t]
                    elseif length(size(dsvar)) == 2
                        varcheck["values"] = varcheck["values"] && checkfld.values[t] == dsvar[:,t]
                    end
                end
            end
        else
            varcheck["values"] = varcheck["values"] && checkfld.values == dsvar
        end
    elseif isa(checkfld.values,BinData)
        varcheck["values"] = true
        for t in 1:length(checkfld.values.fnames)
            testdata = readbin(checkfld.values,t)
            if length(size(dsvar)) == 3
                varcheck["values"] = varcheck["values"] && testdata == dsvar[:,:,t]
            elseif length(size(dsvar)) == 4
                varcheck["values"] = varcheck["values"] && testdata == dsvar[:,:,:,t]
            elseif length(size(dsvar)) == 2
                varcheck["values"] = varcheck["values"] && testdata == dsvar[:,t]
            end
        end
    elseif isa(checkfld.values,NCData)
        varcheck["values"] = true
        testfld = Dataset(checkfld.values.fname)[checkfld.values.varname]
        for t in 1:size(testfld)[end]
            if length(size(dsvar)) == 3
                varcheck["values"] = varcheck["values"] && testfld[:,:,t] == dsvar[:,:,t]
            elseif length(size(dsvar)) == 4
                varcheck["values"] = varcheck["values"] && testfld[:,:,:,t] == dsvar[:,:,:,t]
            elseif length(size(dsvar)) == 2
                varcheck["values"] = varcheck["values"] && testfld[:,t] == dsvar[:,t]
            end
        end
    elseif isa(checkfld.values,TileData)
        itile = Int(ds.attrib["itile"])
        varcheck["values"] = true
        grid = checkfld.values.tileinfo["XC"].grid
        gridvars = GridLoad(grid; option="full")
        land = gridvars.hFacC
        for f in land.fIndex
            for d in 1:size(land,2)
                land[f,d][land[f,d].==0] .= NaN
                land[f,d][land[f,d].>0] .= 1
            end
        end
        if isa(checkfld.values.vals,BinData)
            for t in 1:length(checkfld.values.vals.fnames)
                data = read(readbin(checkfld.values.vals.fnames[t],
                                    checkfld.values.vals.precision,checkfld.values.vals.iosize,
                                    checkfld.values.vals.fldidx),checkfld.values.tileinfo["XC"])
                testdata = NCTiles.gettile(data,checkfld.values.tileinfo,checkfld.values.tilesize,itile)
                landtile = NCTiles.gettile(land,checkfld.values.tileinfo,checkfld.values.tilesize,itile)
                testdata = applylandmask(testdata,landtile)
                if length(size(dsvar)) == 3
                    varcheck["values"] = varcheck["values"] && isequal(testdata,dsvar[:,:,t])
                elseif length(size(dsvar)) == 4
                    varcheck["values"] = varcheck["values"] && isequal(testdata,dsvar[:,:,:,t])
                elseif length(size(dsvar)) == 2
                    varcheck["values"] = varcheck["values"] && isequal(testdata,dsvar[:,t])
                end
            end
        else
            testdata = NCTiles.gettile(checkfld.values.vals,checkfld.values.tileinfo,checkfld.values.tilesize,itile)
            varcheck["values"] = varcheck["values"] && ((testdata .== dsvar) + isnan.(dsvar)) == ones(Int,size(dsvar)...)
        end
    end
    pass = pass && varcheck["values"]

    #4. printout message if >0 test failed
    if ~pass
        missmatchstring = ""
        for k in keys(varcheck)
            if k == "dims"
                for d in keys(varcheck[k])
                    for a in keys(varcheck[k][d])
                        if ~varcheck[k][d][a]
                            missmatchstring = join([missmatchstring,join([k,d,a],'-')],", ")
                        end
                    end
                end
            else
                if ~varcheck[k]
                    missmatchstring = join([missmatchstring,k],", ")
                end
            end
        end
        println("The following file elements do not match the field "*checkfld.name*" elements:\n"*missmatchstring[3:end])
    end
    return pass
end

"""
    applylandmask(data,land)

Apply land mask either lazily (NCvar) or eagerly (other input types).
"""
function applylandmask(data,land)
    if isa(data,NCvar)
        if length(data.dims) == 2
                dataout = NCTiles.replacevalues(
                NCTiles.replacevalues(data.values.*land[:,1],data.values),
                data)
        else
            dataout = NCTiles.replacevalues(
                NCTiles.replacevalues(data.values.*land,data.values),
                data)
        end
    else
        if ndims(data) == 2
            dataout = data.*land[:,:,1]
        else
            dataout = data.*land
        end
    end
    return dataout
end

"""
    maketestdata()

Create test data on a simple, coarse-grained Earth grid.
"""
function maketestdata()
    lon=-180:20:180; lat=-90:20:90;
    depth = 5:10:100
    n1,n2,n3 = (length(lon),length(lat),length(depth))

    timeinterval = 1
    nsteps = 5
    time_steps = timeinterval/2:timeinterval:timeinterval*nsteps
    time_units = "days from 1992-01-01"

    data2d = [ones(Float32,n1,n2) for t in 1:nsteps]
    data3d = [ones(Float32,n1,n2,n3) for t in 1:nsteps]
    units = "test_units"
    longname2d = "Two Dimensional Test Data"
    longname3d = "Three Dimensional Test Data"

    dims = [NCvar("lon_c","degrees_east",n1,lon,Dict("long_name" => "longitude"),NCDatasets),
            NCvar("lat_c","degrees_north",n2,lat,Dict("long_name" => "latitude"),NCDatasets),
            NCvar("dep_c","m",n3,depth,Dict("long_name" => "depth","standard_name" => "depth","positive" => "down"),NCDatasets),
            NCvar("time","days from 1992-01-01",Inf,collect(time_steps),Dict(("long_name" => "time","standard_name" => "time")),NCDatasets)
            ]

    #fnames2d = ["testdata/data2d."*lpad(t,2,"0")*".data" for t in 1:length(data2d)]
    #fnames3d = ["testdata/data3d."*lpad(t,2,"0")*".data" for t in 1:length(data3d)]

    fnames2d = [mktemp()[1] for t in 1:length(data2d)]
    fnames3d = [mktemp()[1] for t in 1:length(data3d)]
    fnamestile2d = [mktemp()[1] for t in 1:length(data2d)]

    for t in 1:length(data2d)
        write(fnames2d[t],hton.(data2d[t]))
    end

    for t in 1:length(data3d)
        write(fnames3d[t],hton.(data3d[t]))
    end

    tilesize = (16,16)
    grid = GridSpec("CubeSphere",getgrid())
    for f in fnamestile2d
        cp(joinpath(grid.path,"Depth.data"),f,force=true)
    end
    dep = -1 .* collect([-5 -15 -25 -35 -45 -55 -65 -75.0049972534180 -85.0250015258789 -95.0950012207031 -105.309997558594 -115.870002746582 -127.150001525879 -139.740005493164 -154.470001220703 -172.399993896484 -194.735000610352 -222.710006713867 -257.470001220703 -299.929992675781 -350.679992675781 -409.929992675781 -477.470001220703 -552.710021972656 -634.734985351563 -722.400024414063 -814.469970703125 -909.739990234375 -1007.15502929688 -1105.90502929688 -1205.53503417969 -1306.20495605469 -1409.15002441406  -1517.09497070313 -1634.17504882813 -1765.13500976563 -1914.15002441406 -2084.03491210938 -2276.22509765625 -2491.25000000000 -2729.25000000000 -2990.25000000000 -3274.25000000000 -3581.25000000000 -3911.25000000000 -4264.25000000000 -4640.25000000000 -5039.25000000000 -5461.25000000000 -5906.25000000000]')
    tile_ex = Dict(["tilesize" => tilesize,
                    "grid" => grid,
                    "dims" =>   [NCvar("i_c","1",tilesize[1],1:tilesize[1],Dict("long_name" => "Cartesian coordinate 1"),NCDatasets),
                                NCvar("j_c","1",tilesize[2],1:tilesize[2],Dict("long_name" => "Cartesian coordinate 2"),NCDatasets),
                                NCvar("dep_c","m",size(dep),dep,Dict("long_name" => "depth","standard_name" => "depth","positive" => "down"),NCDatasets),
                                NCvar("tim","days since 1992-1-1 0:0:0",Inf,time_steps,Dict(("long_name" => "time","standard_name" => "time")),NCDatasets)],
                    "fnamestile2d" => fnamestile2d
                    ])

    return Dict(["data2d" => data2d, "data3d" => data3d,
                 "units" => units, "dims" => dims,
                 "longname2d" => longname2d,"longname3d" => longname3d,
                 "fnames2d" => fnames2d, "fnames3d" => fnames3d, "tile_ex" => tile_ex])

end

"""
    getgrid()

Download (if not already done) real ocean model grid (GRID_CS32) for testing pkg.
"""
function getgrid()

    testdir = abspath(joinpath(dirname(pathof(NCTiles)),"..","test"))
    griddir = joinpath(testdir,"GRID_CS32/")
    if ~ispath(griddir)
        try
            file = joinpath(testdir,"GRID_CS32.tar.gz")
            download("https://github.com/gaelforget/GRID_CS32/archive/master.tar.gz",file)
            run(`tar -xzf $file -C $testdir`)
            olddir = joinpath(testdir,"GRID_CS32-master")
            run(`mv $olddir $griddir`)
            run(`rm $file`)
        catch e
            println("Could not download and extract grid. To fix, get the grid from https://github.com/gaelforget/GRID_CS32/ and put it in the "*testdir*" directory.")
        end
    end

    return griddir
end


# This file is a part of Julia. License is MIT: http://julialang.org/license

module MPFR3

export
    BigFloat,
    setprecision,
    big_str

import
    Base: @big_str, *, +, -, /, <, <=, ==, >, >=, ^,
          besselj, besselj0, besselj1, bessely, bessely0, bessely1,
          ceil, cmp, convert, copysign, div, cbrt, typemax, typemin,
          unsafe_trunc, realmin, realmax, rounding,
          cosh, sinh, tanh, sech, csch, coth, acosh, asinh, atanh, atan2,
          eps, signbit, sin, cos, tan, sec, csc, cot, acos, asin, atan,
          exp, exp2, exponent, factorial, floor, fma, hypot, isinteger,
          gamma, lgamma, digamma, erf, erfc, zeta, eta, airyai,
          isfinite, isinf, isnan, ldexp, log, log2, log10, log1p, exp10, expm1,
          max, min, mod, modf, nextfloat, prevfloat, promote_rule, rem, round,
          show, setrounding, maxintfloat, widen, significand, frexp, tryparse,
          sum, sqrt, string, print, trunc, precision, setprecision

import Base.Rounding: rounding_raw, setrounding_raw

import Base.GMP: ClongMax, CulongMax, CdoubleMax, Limb

import Base.Math.lgamma_r

function __init__()
    try
        # set exponent to full range by default
        set_emin!(get_emin_min())
        set_emax!(get_emax_max())
    catch ex
        Base.showerror_nostdio(ex,
            "WARNING: Error during initialization of module MPFR")
    end
end

const ROUNDING_MODE = Ref{Cint}(0)
const DEFAULT_PRECISION = [256]

# Basic type and initialization definitions

type BigFloat{P} <: AbstractFloat
    prec::Clong
    sign::Cint
    exp::Clong
    d::Ptr{Limb}
    function BigFloat()
    N = Clong(P)
    z = BigFloat{P}(zero(Clong), zero(Cint), zero(Clong), C_NULL)
    ccall((:mpfr_init2,:libmpfr), Void, (Ptr{BigFloat}, Clong), &z, N)
    finalizer(z, cglobal((:mpfr_clear, :libmpfr)))
    return z
    end
    # Not recommended for general use
    function BigFloat(prec::Clong, sign::Cint, exp::Clong, d::Ptr{Void})
        new(prec, sign, exp, d)
    end
end

precision{P}(::Type{BigFloat{P}}) = P
precision{P}(x::BigFloat{P}) = P

function initializer{P}(::Type{BigFloat{P}})
    N = Clong(P)
    z = BigFloat{P}(zero(Clong), zero(Cint), zero(Clong), C_NULL)
    ccall((:mpfr_init2,:libmpfr), Void, (Ptr{BigFloat}, Clong), &z, N)
    finalizer(z, cglobal((:mpfr_clear, :libmpfr)))
    return z
end
initializer(::Type{BigFloat}) = initializer(BigFloat{precision(BigFloat)})

BigFloat{P}(::Type{BigFloat{P}}) = initializer(BigFloat{P})
BigFloat() = initializer(BigFloat)
function BigFloat(prec::Int64, sign::Int32, exp::Int64, d::Ptr{Void})
    P = Int(prec)
    return BigFloat{P}(prec, sign, exp, d)
end

widen(::Type{Float64}) = BigFloat
widen(::Type{BigFloat}) = BigFloat

convert{BF<:BigFloat}(::Type{BF}, x::BF) = x

# convert to BF
for (fJ, fC) in ((:si,:Clong), (:ui,:Culong), (:d,:Float64))
    @eval begin
        function convert{BF<:BigFloat}(::Type{BF}, x::($fC))
            z = BF()
            ccall(($(string(:mpfr_set_,fJ)), :libmpfr), Int32, (Ptr{BF}, ($fC), Int32), &z, x, ROUNDING_MODE[])
            return z
        end
    end
end

function convert{BF<:BigFloat}(::Type{BF}, x::BigInt)
    z = BF()
    ccall((:mpfr_set_z, :libmpfr), Int32, (Ptr{BF}, Ptr{BigInt}, Int32), &z, &x, ROUNDING_MODE[])
    return z
end

convert{BF<:BigFloat}(::Type{BF}, x::Integer) = BF(BigInt(x))

convert{BF<:BigFloat}(::Type{BF}, x::Union{Bool,Int8,Int16,Int32}) = BF(convert(Clong,x))
convert{BF<:BigFloat}(::Type{BF}, x::Union{UInt8,UInt16,UInt32}) = BF(convert(Culong,x))

convert{BF<:BigFloat}(::Type{BF}, x::Union{Float16,Float32}) = BF(Float64(x))
convert{BF<:BigFloat}(::Type{BF}, x::Rational) = BF(num(x)) / BF(den(x))

function tryparse{BF<:BigFloat}(::Type{BF}, s::AbstractString, base::Int=0)
    z = BF()
    err = ccall((:mpfr_set_str, :libmpfr), Int32, (Ptr{BF}, Cstring, Int32, Int32), &z, s, base, ROUNDING_MODE[])
    err == 0 ? Nullable(z) : Nullable{BF}()
end

convert{BF<:BigFloat}(::Type{Rational}, x::BF) = convert(Rational{BigInt}, x)
convert(::Type{AbstractFloat}, x::BigInt) = BigFloat(x)

## BF -> Integer
function unsafe_cast{BF<:BigFloat}(::Type{Int64}, x::BF, ri::Cint)
    ccall((:__gmpfr_mpfr_get_sj,:libmpfr), Cintmax_t,
          (Ptr{BF}, Cint), &x, ri)
end
function unsafe_cast{BF<:BigFloat}(::Type{UInt64}, x::BF, ri::Cint)
    ccall((:__gmpfr_mpfr_get_uj,:libmpfr), Cuintmax_t,
          (Ptr{BF}, Cint), &x, ri)
end

function unsafe_cast{T<:Signed,BF<:BigFloat}(::Type{T}, x::BF, ri::Cint)
    unsafe_cast(Int64, x, ri) % T
end
function unsafe_cast{T<:Unsigned,BF<:BigFloat}(::Type{T}, x::BF, ri::Cint)
    unsafe_cast(UInt64, x, ri) % T
end

function unsafe_cast{BF<:BigFloat}(::Type{BigInt}, x::BF, ri::Cint)
    # actually safe, just keep naming consistent
    z = BigInt()
    ccall((:mpfr_get_z, :libmpfr), Int32, (Ptr{BigInt}, Ptr{BF}, Int32),
          &z, &x, ri)
    z
end
unsafe_cast{BF<:BigFloat}(::Type{Int128}, x::BF, ri::Cint) = Int128(unsafe_cast(BigInt,x,ri))
unsafe_cast{BF<:BigFloat}(::Type{UInt128}, x::BF, ri::Cint) = UInt128(unsafe_cast(BigInt,x,ri))
unsafe_cast{T<:Integer,BF<:BigFloat}(::Type{T}, x::BF, r::RoundingMode) = unsafe_cast(T,x,to_mpfr(r))

unsafe_trunc{T<:Integer,BF<:BigFloat}(::Type{T}, x::BF) = unsafe_cast(T,x,RoundToZero)

function trunc{T<:Union{Signed,Unsigned},BF<:BigFloat}(::Type{T}, x::BF)
    (typemin(T) <= x <= typemax(T)) || throw(InexactError())
    unsafe_cast(T,x,RoundToZero)
end
function floor{T<:Union{Signed,Unsigned},BF<:BigFloat}(::Type{T}, x::BF)
    (typemin(T) <= x <= typemax(T)) || throw(InexactError())
    unsafe_cast(T,x,RoundDown)
end
function ceil{T<:Union{Signed,Unsigned},BF<:BigFloat}(::Type{T}, x::BF)
    (typemin(T) <= x <= typemax(T)) || throw(InexactError())
    unsafe_cast(T,x,RoundUp)
end

function round{T<:Union{Signed,Unsigned},BF<:BigFloat}(::Type{T}, x::BF)
    (typemin(T) <= x <= typemax(T)) || throw(InexactError())
    unsafe_cast(T,x,ROUNDING_MODE[])
end

trunc{BF<:BigFloat}(::Type{BigInt}, x::BF) = unsafe_cast(BigInt, x, RoundToZero)
floor{BF<:BigFloat}(::Type{BigInt}, x::BF) = unsafe_cast(BigInt, x, RoundDown)
ceil{BF<:BigFloat}(::Type{BigInt}, x::BF) = unsafe_cast(BigInt, x, RoundUp)
round{BF<:BigFloat}(::Type{BigInt}, x::BF) = unsafe_cast(BigInt, x, ROUNDING_MODE[])

# convert/round/trunc/floor/ceil(Integer, x) should return a BigInt
trunc{BF<:BigFloat}(::Type{Integer}, x::BF) = trunc(BigInt, x)
floor{BF<:BigFloat}(::Type{Integer}, x::BF) = floor(BigInt, x)
ceil{BF<:BigFloat}(::Type{Integer}, x::BF) = ceil(BigInt, x)
round{BF<:BigFloat}(::Type{Integer}, x::BF) = round(BigInt, x)

convert{BF<:BigFloat}(::Type{Bool}, x::BF) = x==0 ? false : x==1 ? true : throw(InexactError())
function convert{BF<:BigFloat}(::Type{BigInt},x::BF)
    isinteger(x) || throw(InexactError())
    trunc(BigInt,x)
end

function convert{T<:Integer,BF<:BigFloat}(::Type{T},x::BF)
    isinteger(x) || throw(InexactError())
    trunc(T,x)
end

## BF -> AbstractFloat
convert{BF<:BigFloat}(::Type{Float64}, x::BF) =
    ccall((:mpfr_get_d,:libmpfr), Float64, (Ptr{BF},Int32), &x, ROUNDING_MODE[])
convert{BF<:BigFloat}(::Type{Float32}, x::BF) =
    ccall((:mpfr_get_flt,:libmpfr), Float32, (Ptr{BF},Int32), &x, ROUNDING_MODE[])
# TODO: avoid double rounding
convert{BF<:BigFloat}(::Type{Float16}, x::BF) = convert(Float16, convert(Float32, x))

(::Type{Float64}){BF<:BigFloat}(x::BF, r::RoundingMode) =
    ccall((:mpfr_get_d,:libmpfr), Float64, (Ptr{BF},Int32), &x, to_mpfr(r))
(::Type{Float32}){BF<:BigFloat}(x::BF, r::RoundingMode) =
    ccall((:mpfr_get_flt,:libmpfr), Float32, (Ptr{BF},Int32), &x, to_mpfr(r))
# TODO: avoid double rounding
(::Type{Float16}){BF<:BigFloat}(x::BF, r::RoundingMode) =
    convert(Float16, Float32(x, r))

promote_rule(::Type{BigInt},::Type{BigFloat}) = BigFloat
promote_rule{T<:Real}(::Type{BigFloat}, ::Type{T}) = BigFloat
promote_rule{T<:AbstractFloat}(::Type{BigInt},::Type{T}) = BigFloat
promote_rule{T<:AbstractFloat}(::Type{BigFloat},::Type{T}) = BigFloat

promote_rule{P}(::Type{BigInt},::Type{BigFloat{P}}) = BigFloat{P}
promote_rule{BF<:BigFloat}(::Type{BigInt},::Type{BF}) = BF
promote_rule{T<:Real,BF<:BigFloat}(::Type{BF}, ::Type{T}) = BF
promote_rule{T<:AbstractFloat,BF<:BigFloat}(::Type{BF},::Type{T}) = BF

function convert(::Type{Rational{BigInt}}, x::AbstractFloat)
    if isnan(x); return zero(BigInt)//zero(BigInt); end
    if isinf(x); return copysign(one(BigInt),x)//zero(BigInt); end
    if x == 0;   return zero(BigInt) // one(BigInt); end
    s = max(precision(x) - exponent(x), 0)
    BigInt(ldexp(x,s)) // (BigInt(1) << s)
end

# Basic arithmetic without promotion
for (fJ, fC) in ((:+,:add), (:*,:mul))
    @eval begin
        # BF
        function ($fJ){BF<:BigFloat}(x::BF, y::BF)
            z = BF()
            ccall(($(string(:mpfr_,fC)),:libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BF}, Int32), &z, &x, &y, ROUNDING_MODE[])
            return z
        end

        # Unsigned Integer
        function ($fJ){BF<:BigFloat}(x::BF, c::CulongMax)
            z = BF()
            ccall(($(string(:mpfr_,fC,:_ui)), :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Culong, Int32), &z, &x, c, ROUNDING_MODE[])
            return z
        end
        ($fJ){BF<:BigFloat}(c::CulongMax, x::BF) = ($fJ)(x,c)

        # Signed Integer
        function ($fJ){BF<:BigFloat}(x::BF, c::ClongMax)
            z = BF()
            ccall(($(string(:mpfr_,fC,:_si)), :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Clong, Int32), &z, &x, c, ROUNDING_MODE[])
            return z
        end
        ($fJ){BF<:BigFloat}(c::ClongMax, x::BF) = ($fJ)(x,c)

        # Float32/Float64
        function ($fJ){BF<:BigFloat}(x::BF, c::CdoubleMax)
            z = BF()
            ccall(($(string(:mpfr_,fC,:_d)), :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Cdouble, Int32), &z, &x, c, ROUNDING_MODE[])
            return z
        end
        ($fJ){BF<:BigFloat}(c::CdoubleMax, x::BF) = ($fJ)(x,c)

        # BigInt
        function ($fJ){BF<:BigFloat}(x::BF, c::BigInt)
            z = BF()
            ccall(($(string(:mpfr_,fC,:_z)), :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BigInt}, Int32), &z, &x, &c, ROUNDING_MODE[])
            return z
        end
        ($fJ){BF<:BigFloat}(c::BigInt, x::BF) = ($fJ)(x,c)
    end
end

for (fJ, fC) in ((:-,:sub), (:/,:div))
    @eval begin
        # BF
        function ($fJ){BF<:BigFloat}(x::BF, y::BF)
            z = BF()
            ccall(($(string(:mpfr_,fC)),:libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BF}, Int32), &z, &x, &y, ROUNDING_MODE[])
            return z
        end

        # Unsigned Int
        function ($fJ){BF<:BigFloat}(x::BF, c::CulongMax)
            z = BF()
            ccall(($(string(:mpfr_,fC,:_ui)), :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Culong, Int32), &z, &x, c, ROUNDING_MODE[])
            return z
        end
        function ($fJ){BF<:BigFloat}(c::CulongMax, x::BF)
            z = BF()
            ccall(($(string(:mpfr_,:ui_,fC)), :libmpfr), Int32, (Ptr{BF}, Culong, Ptr{BF}, Int32), &z, c, &x, ROUNDING_MODE[])
            return z
        end

        # Signed Integer
        function ($fJ){BF<:BigFloat}(x::BF, c::ClongMax)
            z = BF()
            ccall(($(string(:mpfr_,fC,:_si)), :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Clong, Int32), &z, &x, c, ROUNDING_MODE[])
            return z
        end
        function ($fJ){BF<:BigFloat}(c::ClongMax, x::BF)
            z = BF()
            ccall(($(string(:mpfr_,:si_,fC)), :libmpfr), Int32, (Ptr{BF}, Clong, Ptr{BF}, Int32), &z, c, &x, ROUNDING_MODE[])
            return z
        end

        # Float32/Float64
        function ($fJ){BF<:BigFloat}(x::BF, c::CdoubleMax)
            z = BF()
            ccall(($(string(:mpfr_,fC,:_d)), :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Cdouble, Int32), &z, &x, c, ROUNDING_MODE[])
            return z
        end
        function ($fJ){BF<:BigFloat}(c::CdoubleMax, x::BF)
            z = BF()
            ccall(($(string(:mpfr_,:d_,fC)), :libmpfr), Int32, (Ptr{BF}, Cdouble, Ptr{BF}, Int32), &z, c, &x, ROUNDING_MODE[])
            return z
        end

        # BigInt
        function ($fJ){BF<:BigFloat}(x::BF, c::BigInt)
            z = BF()
            ccall(($(string(:mpfr_,fC,:_z)), :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BigInt}, Int32), &z, &x, &c, ROUNDING_MODE[])
            return z
        end
        # no :mpfr_z_div function
    end
end

function -{BF<:BigFloat}(c::BigInt, x::BF)
    z = BF()
    ccall((:mpfr_z_sub, :libmpfr), Int32, (Ptr{BF}, Ptr{BigInt}, Ptr{BF}, Int32), &z, &c, &x, ROUNDING_MODE[])
    return z
end

function fma{BF<:BigFloat}(x::BF, y::BF, z::BF)
    r = BF()
    ccall(("mpfr_fma",:libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BF}, Ptr{BF}, Int32), &r, &x, &y, &z, ROUNDING_MODE[])
    return r
end

# div
# BF
function div{BF<:BigFloat}(x::BF, y::BF)
    z = BF()
    ccall((:mpfr_div,:libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BF}, Int32), &z, &x, &y, to_mpfr(RoundToZero))
    ccall((:mpfr_trunc, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}), &z, &z)
    return z
end

# Unsigned Int
function div{BF<:BigFloat}(x::BF, c::CulongMax)
    z = BF()
    ccall((:mpfr_div_ui, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Culong, Int32), &z, &x, c, to_mpfr(RoundToZero))
    ccall((:mpfr_trunc, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}), &z, &z)
    return z
end
function div{BF<:BigFloat}(c::CulongMax, x::BF)
    z = BF()
    ccall((:mpfr_ui_div, :libmpfr), Int32, (Ptr{BF}, Culong, Ptr{BF}, Int32), &z, c, &x, to_mpfr(RoundToZero))
    ccall((:mpfr_trunc, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}), &z, &z)
    return z
end

# Signed Integer
function div{BF<:BigFloat}(x::BF, c::ClongMax)
    z = BF()
    ccall((:mpfr_div_si, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Clong, Int32), &z, &x, c, to_mpfr(RoundToZero))
    ccall((:mpfr_trunc, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}), &z, &z)
    return z
end
function div{BF<:BigFloat}(c::ClongMax, x::BF)
    z = BF()
    ccall((:mpfr_si_div, :libmpfr), Int32, (Ptr{BF}, Clong, Ptr{BF}, Int32), &z, c, &x, to_mpfr(RoundToZero))
    ccall((:mpfr_trunc, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}), &z, &z)
    return z
end

# Float32/Float64
function div{BF<:BigFloat}(x::BF, c::CdoubleMax)
    z = BF()
    ccall((:mpfr_div_d, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Cdouble, Int32), &z, &x, c, to_mpfr(RoundToZero))
    ccall((:mpfr_trunc, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}), &z, &z)
    return z
end
function div{BF<:BigFloat}(c::CdoubleMax, x::BF)
    z = BF()
    ccall((:mpfr_d_div, :libmpfr), Int32, (Ptr{BF}, Cdouble, Ptr{BF}, Int32), &z, c, &x, to_mpfr(RoundToZero))
    ccall((:mpfr_trunc, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}), &z, &z)
    return z
end

# BigInt
function div{BF<:BigFloat}(x::BF, c::BigInt)
    z = BF()
    ccall((:mpfr_div_z, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BigInt}, Int32), &z, &x, &c, to_mpfr(RoundToZero))
    ccall((:mpfr_trunc, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}), &z, &z)
    return z
end

# More efficient commutative operations
for (fJ, fC, fI) in ((:+, :add, 0), (:*, :mul, 1))
    @eval begin
        function ($fJ){BF<:BigFloat}(a::BF, b::BF, c::BF)
            z = BF()
            ccall(($(string(:mpfr_,fC)), :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BF}, Int32), &z, &a, &b, ROUNDING_MODE[])
            ccall(($(string(:mpfr_,fC)), :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BF}, Int32), &z, &z, &c, ROUNDING_MODE[])
            return z
        end
        function ($fJ){BF<:BigFloat}(a::BF, b::BF, c::BF, d::BF)
            z = BF()
            ccall(($(string(:mpfr_,fC)), :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BF}, Int32), &z, &a, &b, ROUNDING_MODE[])
            ccall(($(string(:mpfr_,fC)), :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BF}, Int32), &z, &z, &c, ROUNDING_MODE[])
            ccall(($(string(:mpfr_,fC)), :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BF}, Int32), &z, &z, &d, ROUNDING_MODE[])
            return z
        end
        function ($fJ){BF<:BigFloat}(a::BF, b::BF, c::BF, d::BF, e::BF)
            z = BF()
            ccall(($(string(:mpfr_,fC)), :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BF}, Int32), &z, &a, &b, ROUNDING_MODE[])
            ccall(($(string(:mpfr_,fC)), :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BF}, Int32), &z, &z, &c, ROUNDING_MODE[])
            ccall(($(string(:mpfr_,fC)), :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BF}, Int32), &z, &z, &d, ROUNDING_MODE[])
            ccall(($(string(:mpfr_,fC)), :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BF}, Int32), &z, &z, &e, ROUNDING_MODE[])
            return z
        end
    end
end

function -{BF<:BigFloat}(x::BF)
    z = BF()
    ccall((:mpfr_neg, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Int32), &z, &x, ROUNDING_MODE[])
    return z
end

function sqrt{BF<:BigFloat}(x::BF)
    isnan(x) && return x
    z = BF()
    ccall((:mpfr_sqrt, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Int32), &z, &x, ROUNDING_MODE[])
    if isnan(z)
        throw(DomainError())
    end
    return z
end

sqrt(x::BigInt) = sqrt(BF(x))

function ^{BF<:BigFloat}(x::BF, y::BF)
    z = BF()
    ccall((:mpfr_pow, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BF}, Int32), &z, &x, &y, ROUNDING_MODE[])
    return z
end

function ^{BF<:BigFloat}(x::BF, y::CulongMax)
    z = BF()
    ccall((:mpfr_pow_ui, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Culong, Int32), &z, &x, y, ROUNDING_MODE[])
    return z
end

function ^{BF<:BigFloat}(x::BF, y::ClongMax)
    z = BF()
    ccall((:mpfr_pow_si, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Clong, Int32), &z, &x, y, ROUNDING_MODE[])
    return z
end

function ^{BF<:BigFloat}(x::BF, y::BigInt)
    z = BF()
    ccall((:mpfr_pow_z, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BigInt}, Int32), &z, &x, &y, ROUNDING_MODE[])
    return z
end

^{BF<:BigFloat}(x::BF, y::Integer)  = typemin(Clong)  <= y <= typemax(Clong)  ? x^Clong(y)  : x^BigInt(y)
^{BF<:BigFloat}(x::BF, y::Unsigned) = typemin(Culong) <= y <= typemax(Culong) ? x^Culong(y) : x^BigInt(y)

for f in (:exp, :exp2, :exp10, :expm1, :digamma, :erf, :erfc, :zeta,
          :cosh,:sinh,:tanh,:sech,:csch,:coth, :cbrt)
    @eval function $f{BF<:BigFloat}(x::BF)
        z = BF()
        ccall(($(string(:mpfr_,f)), :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Int32), &z, &x, ROUNDING_MODE[])
        return z
    end
end

# return log(2)
function big_ln2()
    c = BF()
    ccall((:mpfr_const_log2, :libmpfr), Cint, (Ptr{BF}, Int32),
          &c, MPFR.ROUNDING_MODE[])
    return c
end

function eta{BF<:BigFloat}(x::BF)
    x == 1 && return big_ln2()
    return -zeta(x) * expm1(big_ln2()*(1-x))
end

function airyai{BF<:BigFloat}(x::BF)
    z = BF()
    ccall((:mpfr_ai, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Int32), &z, &x, ROUNDING_MODE[])
    return z
end
airy{BF<:BigFloat}(x::BF) = airyai(x)

function ldexp{BF<:BigFloat}(x::BF, n::Clong)
    z = BF()
    ccall((:mpfr_mul_2si, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Clong, Int32), &z, &x, n, ROUNDING_MODE[])
    return z
end
function ldexp{BF<:BigFloat}(x::BF, n::Culong)
    z = BF()
    ccall((:mpfr_mul_2ui, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Culong, Int32), &z, &x, n, ROUNDING_MODE[])
    return z
end
ldexp{BF<:BigFloat}(x::BF, n::ClongMax) = ldexp(x, convert(Clong, n))
ldexp{BF<:BigFloat}(x::BF, n::CulongMax) = ldexp(x, convert(Culong, n))
ldexp{BF<:BigFloat}(x::BF, n::Integer) = x*exp2(BF(n))

function besselj0{BF<:BigFloat}(x::BF)
    z = BF()
    ccall((:mpfr_j0, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Int32), &z, &x, ROUNDING_MODE[])
    return z
end

function besselj1{BF<:BigFloat}(x::BF)
    z = BF()
    ccall((:mpfr_j1, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Int32), &z, &x, ROUNDING_MODE[])
    return z
end

function besselj{BF<:BigFloat}(n::Integer, x::BF)
    z = BF()
    ccall((:mpfr_jn, :libmpfr), Int32, (Ptr{BF}, Clong, Ptr{BF}, Int32), &z, n, &x, ROUNDING_MODE[])
    return z
end

function bessely0{BF<:BigFloat}(x::BF)
    if x < 0
        throw(DomainError())
    end
    z = BF()
    ccall((:mpfr_y0, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Int32), &z, &x, ROUNDING_MODE[])
    return z
end

function bessely1{BF<:BigFloat}(x::BF)
    if x < 0
        throw(DomainError())
    end
    z = BF()
    ccall((:mpfr_y1, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Int32), &z, &x, ROUNDING_MODE[])
    return z
end

function bessely{BF<:BigFloat}(n::Integer, x::BF)
    if x < 0
        throw(DomainError())
    end
    z = BF()
    ccall((:mpfr_yn, :libmpfr), Int32, (Ptr{BF}, Clong, Ptr{BF}, Int32), &z, n, &x, ROUNDING_MODE[])
    return z
end

function factorial{BF<:BigFloat}(x::BF)
    if x < 0 || !isinteger(x)
        throw(DomainError())
    end
    ui = convert(Culong, x)
    z = BF()
    ccall((:mpfr_fac_ui, :libmpfr), Int32, (Ptr{BF}, Culong, Int32), &z, ui, ROUNDING_MODE[])
    return z
end

function hypot{BF<:BigFloat}(x::BF, y::BF)
    z = BF()
    ccall((:mpfr_hypot, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BF}, Int32), &z, &x, &y, ROUNDING_MODE[])
    return z
end

for f in (:log, :log2, :log10)
    @eval function $f{BF<:BigFloat}(x::BF)
        if x < 0
            throw(DomainError())
        end
        z = BF()
        ccall(($(string(:mpfr_,f)), :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Int32), &z, &x, ROUNDING_MODE[])
        return z
    end
end

function log1p{BF<:BigFloat}(x::BF)
    if x < -1
        throw(DomainError())
    end
    z = BF()
    ccall((:mpfr_log1p, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Int32), &z, &x, ROUNDING_MODE[])
    return z
end

function max{BF<:BigFloat}(x::BF, y::BF)
    z = BF()
    ccall((:mpfr_max, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BF}, Int32), &z, &x, &y, ROUNDING_MODE[])
    return z
end

function min{BF<:BigFloat}(x::BF, y::BF)
    z = BF()
    ccall((:mpfr_min, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BF}, Int32), &z, &x, &y, ROUNDING_MODE[])
    return z
end

function modf{BF<:BigFloat}(x::BF)
    if isinf(x)
        return (BF(NaN), x)
    end
    zint = BF()
    zfloat = BF()
    ccall((:mpfr_modf, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BF}, Int32), &zint, &zfloat, &x, ROUNDING_MODE[])
    return (zfloat, zint)
end

function rem{BF<:BigFloat}(x::BF, y::BF)
    z = BF()
    ccall((:mpfr_fmod, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BF}, Int32), &z, &x, &y, ROUNDING_MODE[])
    return z
end

function sum{BF<:BigFloat}(arr::AbstractArray{BF})
    z = BF(0)
    for i in arr
        ccall((:mpfr_add, :libmpfr), Int32,
            (Ptr{BF}, Ptr{BF}, Ptr{BF}, Cint),
            &z, &z, &i, 0)
    end
    return z
end

# Functions for which NaN results are converted to DomainError, following Base
for f in (:sin,:cos,:tan,:sec,:csc,
          :acos,:asin,:atan,:acosh,:asinh,:atanh, :gamma)
    @eval begin
        function ($f){BF<:BigFloat}(x::BF)
            if isnan(x)
                return x
            end
            z = BF()
            ccall(($(string(:mpfr_,f)), :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Int32), &z, &x, ROUNDING_MODE[])
            if isnan(z)
                throw(DomainError())
            end
            return z
        end
    end
end

# log of absolute value of gamma function
const lgamma_signp = Array{Cint}(1)
function lgamma{BF<:BigFloat}(x::BF)
    z = BF()
    ccall((:mpfr_lgamma,:libmpfr), Cint, (Ptr{BF}, Ptr{Cint}, Ptr{BF}, Int32), &z, lgamma_signp, &x, ROUNDING_MODE[])
    return z
end

lgamma_r{BF<:BigFloat}(x::BF) = (lgamma(x), lgamma_signp[1])

function atan2{BF<:BigFloat}(y::BF, x::BF)
    z = BF()
    ccall((:mpfr_atan2, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BF}, Int32), &z, &y, &x, ROUNDING_MODE[])
    return z
end

# Utility functions
=={BF<:BigFloat}(x::BF, y::BF) = ccall((:mpfr_equal_p, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}), &x, &y) != 0
<={BF<:BigFloat}(x::BF, y::BF) = ccall((:mpfr_lessequal_p, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}), &x, &y) != 0
>={BF<:BigFloat}(x::BF, y::BF) = ccall((:mpfr_greaterequal_p, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}), &x, &y) != 0
<{BF<:BigFloat}(x::BF, y::BF) = ccall((:mpfr_less_p, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}), &x, &y) != 0
>{BF<:BigFloat}(x::BF, y::BF) = ccall((:mpfr_greater_p, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}), &x, &y) != 0

function cmp{BF<:BigFloat}(x::BF, y::BigInt)
    isnan(x) && throw(DomainError())
    ccall((:mpfr_cmp_z, :libmpfr), Int32, (Ptr{BF}, Ptr{BigInt}), &x, &y)
end
function cmp{BF<:BigFloat}(x::BF, y::ClongMax)
    isnan(x) && throw(DomainError())
    ccall((:mpfr_cmp_si, :libmpfr), Int32, (Ptr{BF}, Clong), &x, y)
end
function cmp{BF<:BigFloat}(x::BF, y::CulongMax)
    isnan(x) && throw(DomainError())
    ccall((:mpfr_cmp_ui, :libmpfr), Int32, (Ptr{BF}, Culong), &x, y)
end
cmp{BF<:BigFloat}(x::BF, y::Integer) = cmp(x,big(y))
cmp{BF<:BigFloat}(x::Integer, y::BF) = -cmp(y,x)

function cmp{BF<:BigFloat}(x::BF, y::CdoubleMax)
    (isnan(x) || isnan(y)) && throw(DomainError())
    ccall((:mpfr_cmp_d, :libmpfr), Int32, (Ptr{BF}, Cdouble), &x, y)
end
cmp{BF<:BigFloat}(x::CdoubleMax, y::BF) = -cmp(y,x)

=={BF<:BigFloat}(x::BF, y::Integer)   = !isnan(x) && cmp(x,y) == 0
=={BF<:BigFloat}(x::Integer, y::BF)   = y == x
=={BF<:BigFloat}(x::BF, y::CdoubleMax) = !isnan(x) && !isnan(y) && cmp(x,y) == 0
=={BF<:BigFloat}(x::CdoubleMax, y::BF) = y == x

<{BF<:BigFloat}(x::BF, y::Integer)   = !isnan(x) && cmp(x,y) < 0
<{BF<:BigFloat}(x::Integer, y::BF)   = !isnan(y) && cmp(y,x) > 0
<{BF<:BigFloat}(x::BF, y::CdoubleMax) = !isnan(x) && !isnan(y) && cmp(x,y) < 0
<{BF<:BigFloat}(x::CdoubleMax, y::BF) = !isnan(x) && !isnan(y) && cmp(y,x) > 0

<={BF<:BigFloat}(x::BF, y::Integer)   = !isnan(x) && cmp(x,y) <= 0
<={BF<:BigFloat}(x::Integer, y::BF)   = !isnan(y) && cmp(y,x) >= 0
<={BF<:BigFloat}(x::BF, y::CdoubleMax) = !isnan(x) && !isnan(y) && cmp(x,y) <= 0
<={BF<:BigFloat}(x::CdoubleMax, y::BF) = !isnan(x) && !isnan(y) && cmp(y,x) >= 0

signbit{BF<:BigFloat}(x::BF) = ccall((:mpfr_signbit, :libmpfr), Int32, (Ptr{BF},), &x) != 0

function precision{BF<:BigFloat}(x::BF)  # precision of an object of type BF
    return ccall((:mpfr_get_prec, :libmpfr), Clong, (Ptr{BF},), &x)
end

precision{BF<:BigFloat}(::Type{BF}) = DEFAULT_PRECISION[end]  # precision of the type BF itself

"""
    setprecision([T=BF,] precision::Int)

Set the precision (in bits) to be used for `T` arithmetic.
"""
function setprecision(::Type{BigFloat}, precision::Int)
    if precision < 2
        throw(DomainError())
    end
    DEFAULT_PRECISION[end] = precision
end

setprecision(precision::Int) = setprecision(BigFloat, precision)

maxintfloat{BF<:BigFloat}(x::BF) = BF(2)^precision(x)
maxintfloat{BF<:BigFloat}(::Type{BF}) = BF(2)^precision(BF)

to_mpfr(::RoundingMode{:Nearest}) = Cint(0)
to_mpfr(::RoundingMode{:ToZero}) = Cint(1)
to_mpfr(::RoundingMode{:Up}) = Cint(2)
to_mpfr(::RoundingMode{:Down}) = Cint(3)
to_mpfr(::RoundingMode{:FromZero}) = Cint(4)

function from_mpfr(c::Integer)
    if c == 0
        return RoundNearest
    elseif c == 1
        return RoundToZero
    elseif c == 2
        return RoundUp
    elseif c == 3
        return RoundDown
    elseif c == 4
        return RoundFromZero
    else
        throw(ArgumentError("invalid MPFR rounding mode code: $c"))
    end
    RoundingMode(c)
end

rounding_raw{BF<:BigFloat}(::Type{BF}) = ROUNDING_MODE[]
setrounding_raw{BF<:BigFloat}(::Type{BF},i::Integer) = ROUNDING_MODE[] = i

rounding{BF<:BigFloat}(::Type{BF}) = from_mpfr(rounding_raw(BF))
setrounding{BF<:BigFloat}(::Type{BF},r::RoundingMode) = setrounding_raw(BF,to_mpfr(r))

function copysign{BF<:BigFloat}(x::BF, y::BF)
    z = BF()
    ccall((:mpfr_copysign, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Ptr{BF}, Int32), &z, &x, &y, ROUNDING_MODE[])
    return z
end

function exponent{BF<:BigFloat}(x::BF)
    if x == 0 || !isfinite(x)
        throw(DomainError())
    end
    # The '- 1' is to make it work as Base.exponent
    return ccall((:mpfr_get_exp, :libmpfr), Clong, (Ptr{BF},), &x) - 1
end

function frexp{BF<:BigFloat}(x::BF)
    z = BF()
    c = Ref{Clong}()
    ccall((:mpfr_frexp, :libmpfr), Int32, (Ptr{Clong}, Ptr{BF}, Ptr{BF}, Cint), c, &z, &x, ROUNDING_MODE[])
    return (z, c[])
end

function significand{BF<:BigFloat}(x::BF)
    z = BF()
    c = Ref{Clong}()
    ccall((:mpfr_frexp, :libmpfr), Int32, (Ptr{Clong}, Ptr{BF}, Ptr{BF}, Cint), c, &z, &x, ROUNDING_MODE[])
    # Double the significand to make it work as Base.significand
    ccall((:mpfr_mul_si, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Clong, Int32), &z, &z, 2, ROUNDING_MODE[])
    return z
end

function isinteger{BF<:BigFloat}(x::BF)
    return ccall((:mpfr_integer_p, :libmpfr), Int32, (Ptr{BF},), &x) != 0
end

for f in (:ceil, :floor, :trunc)
    @eval begin
        function ($f){BF<:BigFloat}(x::BF)
            z = BF()
            ccall(($(string(:mpfr_,f)), :libmpfr), Int32, (Ptr{BF}, Ptr{BF}), &z, &x)
            return z
        end
    end
end

function round{BF<:BigFloat}(x::BF)
    z = BF()
    ccall((:mpfr_rint, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Cint), &z, &x, ROUNDING_MODE[])
    return z
end
function round{BF<:BigFloat}(x::BF,::RoundingMode{:NearestTiesAway})
    z = BF()
    ccall((:mpfr_round, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}), &z, &x)
    return z
end

function isinf{BF<:BigFloat}(x::BF)
    return ccall((:mpfr_inf_p, :libmpfr), Int32, (Ptr{BF},), &x) != 0
end

function isnan{BF<:BigFloat}(x::BF)
    return ccall((:mpfr_nan_p, :libmpfr), Int32, (Ptr{BF},), &x) != 0
end

isfinite{BF<:BigFloat}(x::BF) = !isinf(x) && !isnan(x)

@eval typemax(::Type{BigFloat}) = $(BigFloat( Inf))
@eval typemin(::Type{BigFloat}) = $(BigFloat(-Inf))
typemax{BF<:BigFloat}(::Type{BF}) = BF( Inf)
typemin{BF<:BigFloat}(::Type{BF}) = BF(-Inf)

function nextfloat{BF<:BigFloat}(x::BF)
    z = BF()
    ccall((:mpfr_set, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Int32),
          &z, &x, ROUNDING_MODE[])
    ccall((:mpfr_nextabove, :libmpfr), Int32, (Ptr{BF},), &z) != 0
    return z
end

function prevfloat{BF<:BigFloat}(x::BF)
    z = BF()
    ccall((:mpfr_set, :libmpfr), Int32, (Ptr{BF}, Ptr{BF}, Int32),
          &z, &x, ROUNDING_MODE[])
    ccall((:mpfr_nextbelow, :libmpfr), Int32, (Ptr{BF},), &z) != 0
    return z
end

eps{BF<:BigFloat}(::Type{BF}) = nextfloat(BF(1)) - BF(1)

realmin{BF<:BigFloat}(::Type{BF}) = nextfloat(zero(BF))
realmax{BF<:BigFloat}(::Type{BF}) = prevfloat(BF(Inf))

"""
    setprecision(f::Function, [T=BF,] precision::Integer)

Change the `T` arithmetic precision (in bits) for the duration of `f`.
It is logically equivalent to:

    old = precision(BF)
    setprecision(BF, precision)
    f()
    setprecision(BF, old)

Often used as `setprecision(T, precision) do ... end`
"""
function setprecision{T}(f::Function, ::Type{T}, prec::Integer)
    old_prec = precision(T)
    setprecision(T, prec)
    try
        return f()
    finally
        setprecision(T, old_prec)
    end
end

setprecision(f::Function, precision::Integer) = setprecision(f, BigFloat, precision)

function string(x::BigFloat)
    # In general, the number of decimal places needed to read back the number exactly
    # is, excluding the most significant, ceil(log(10, 2^precision(x)))
    k = ceil(Int32, precision(x) * 0.3010299956639812)
    lng = k + Int32(8) # Add space for the sign, the most significand digit, the dot and the exponent
    buf = Array{UInt8}(lng + 1)
    # format strings are guaranteed to contain no NUL, so we don't use Cstring
    lng = ccall((:mpfr_snprintf,:libmpfr), Int32, (Ptr{UInt8}, Culong, Ptr{UInt8}, Ptr{BigFloat}...), buf, lng + 1, "%.Re", &x)
    if lng < k + 5 # print at least k decimal places
        lng = ccall((:mpfr_sprintf,:libmpfr), Int32, (Ptr{UInt8}, Ptr{UInt8}, Ptr{BigFloat}...), buf, "%.$(k)Re", &x)
    elseif lng > k + 8
        buf = Array{UInt8}(lng + 1)
        lng = ccall((:mpfr_snprintf,:libmpfr), Int32, (Ptr{UInt8}, Culong, Ptr{UInt8}, Ptr{BigFloat}...), buf, lng + 1, "%.Re", &x)
    end
    n = (1 <= x < 10 || -10 < x <= -1 || x == 0) ? lng - 4 : lng
    return String(buf[1:n])
end

function string{BF<:BigFloat}(x::BF)
    # In general, the number of decimal places needed to read back the number exactly
    # is, excluding the most significant, ceil(log(10, 2^precision(x)))
    k = ceil(Int32, precision(x) * 0.3010299956639812)
    lng = k + Int32(8) # Add space for the sign, the most significand digit, the dot and the exponent
    buf = Array{UInt8}(lng + 1)
    # format strings are guaranteed to contain no NUL, so we don't use Cstring
    lng = ccall((:mpfr_snprintf,:libmpfr), Int32, (Ptr{UInt8}, Culong, Ptr{UInt8}, Ptr{BF}...), buf, lng + 1, "%.Re", &x)
    if lng < k + 5 # print at least k decimal places
        lng = ccall((:mpfr_sprintf,:libmpfr), Int32, (Ptr{UInt8}, Ptr{UInt8}, Ptr{BF}...), buf, "%.$(k)Re", &x)
    elseif lng > k + 8
        buf = Array{UInt8}(lng + 1)
        lng = ccall((:mpfr_snprintf,:libmpfr), Int32, (Ptr{UInt8}, Culong, Ptr{UInt8}, Ptr{BF}...), buf, lng + 1, "%.Re", &x)
    end
    n = (1 <= x < 10 || -10 < x <= -1 || x == 0) ? lng - 4 : lng
    return String(buf[1:n])
end

print(io::IO, b::BigFloat) = print(io, string(b))
show(io::IO, b::BigFloat) = print(io, string(b))
print{BF<:BigFloat}(io::IO, b::BF) = print(io, string(b))
show{BF<:BigFloat}(io::IO, b::BF) = print(io, string(b))

# get/set exponent min/max
get_emax() = ccall((:mpfr_get_emax, :libmpfr), Clong, ())
get_emax_min() = ccall((:mpfr_get_emax_min, :libmpfr), Clong, ())
get_emax_max() = ccall((:mpfr_get_emax_max, :libmpfr), Clong, ())

get_emin() = ccall((:mpfr_get_emin, :libmpfr), Clong, ())
get_emin_min() = ccall((:mpfr_get_emin_min, :libmpfr), Clong, ())
get_emin_max() = ccall((:mpfr_get_emin_max, :libmpfr), Clong, ())

set_emax!(x) = ccall((:mpfr_set_emax, :libmpfr), Void, (Clong,), x)
set_emin!(x) = ccall((:mpfr_set_emin, :libmpfr), Void, (Clong,), x)

function Base.deepcopy_internal(x::BigFloat, stackdict::ObjectIdDict)
    if haskey(stackdict, x)
        return stackdict[x]
    end
    N = precision(x)
    y = BigFloat(zero(Clong), zero(Cint), zero(Clong), C_NULL)
    ccall((:mpfr_init2,:libmpfr), Void, (Ptr{BigFloat}, Clong), &y, N)
    finalizer(y, cglobal((:mpfr_clear, :libmpfr)))
    ccall((:mpfr_set, :libmpfr), Int32, (Ptr{BigFloat}, Ptr{BigFloat}, Int32),
          &y, &x, ROUNDING_MODE[])
    stackdict[x] = y
    return y
end

convert{BF<:BigFloat}(::Type{BF}, x::BF) = x
function convert{BF1<:BigFloat, BF2<:BigFloat}(::Type{BF1}, x::BF2)
    return parse(BF1, string(x))
end
function promote_rule{BF1<:BigFloat, BF2<:BigFloat}(::Type{BF1}, ::Type{BF2})
    return ifelse(precision(BF1) > precision(BF2), BF2, BF1)
end

(+){BF1<:BigFloat, BF2<:BigFloat}(a::BF1, b::BF2) = (+)(promote(a,b)...)
(-){BF1<:BigFloat, BF2<:BigFloat}(a::BF1, b::BF2) = (-)(promote(a,b)...)
(*){BF1<:BigFloat, BF2<:BigFloat}(a::BF1, b::BF2) = (*)(promote(a,b)...)
(/){BF1<:BigFloat, BF2<:BigFloat}(a::BF1, b::BF2) = (/)(promote(a,b)...)
(hypot){BF1<:BigFloat, BF2<:BigFloat}(a::BF1, b::BF2) = (hypot)(promote(a,b)...)
(^){BF1<:BigFloat, BF2<:BigFloat}(a::BF1, b::BF2) = (^)(promote(a,b)...)

end #module


## -----------  School types

abstract type AbstractSchoolGroups{I1 <: Integer} end

# Map :HSG => 1 etc
function ed_idx(s :: AbstractSchoolGroups{I1}, ed :: Symbol) where I1
    edIdx = findfirst(ed_symbols(s) .== ed);
    @assert  (!isnothing(edIdx)  && edIdx > 0)  "Invalid $ed"
    return edIdx
end

ed_idx(s :: AbstractSchoolGroups{I1}, ed :: Integer) where I1 = I1(ed);

# String labels
ed_labels(s :: AbstractSchoolGroups{I1}) where I1 = string.(ed_symbols(s));
ed_label(s :: AbstractSchoolGroups{I1}, ed :: Symbol) where I1 =
    ed_labels(s)[ed_idx(s, ed)];
ed_label(s :: AbstractSchoolGroups{I1}, ed :: Integer) where I1 = ed_labels(s)[ed];
ed_symbol(s :: AbstractSchoolGroups{I1}, idx) where I1 = ed_symbols(s)[idx];

# Suffixes of the form "_1HSG" for files
ed_suffixes(s :: AbstractSchoolGroups{I1}) where I1 =
    [ed_suffix(s, j)  for j = 1 : n_school(s)];

function ed_suffix(s :: AbstractSchoolGroups{I1}, ed) where I1
    edIdx = ed_idx(s, ed);
    edStr = ed_label(s, ed);
    return "_$edIdx$edStr"
end

# ------- HSG, CD, CG
struct Schooling3{I1} <: AbstractSchoolGroups{I1}

end

Base.show(io :: IO, s :: Schooling3{I1}) where I1 =
    print(io, "Schooling: HSG, SC, CG");

StructLH.describe(s :: Schooling3{I1}) where I1 =
    ["Schooling:"  "HSG, SC, CG"];

n_school(s :: Schooling3{I1}) where I1 = I1(3);

ed_symbols(s :: Schooling3) = [:HSG, :SC, :CG];

# ---------------


import Base: ==, convert, Dict

const zklmTuple = NamedTuple{(:z, :k, :l, :m), Tuple{Int16, IntS, IntS, IntS}}

"""
`AList` : datastructure to help compute the A_zklm density projections

* `i2zklm` : list of all admissible `(z,k,l,m)` tuples
* `zklm2i` : dictionary with (z,k,l,m) keys to compute  the map `(z,k,l,m) -> i`
* `firstz` : `firstz[iz]` stores the first index in the A_zklm array for with
             z = zi. This can be used to iterate over all A entries for which
             z = zi. (they are sorted by z first)
"""
struct AList
   i2zklm::Vector{zklmTuple}
   zklm2i::Dict{zklmTuple, IntS}
   firstz::Vector{IntS}
end

# --------------(de-)serialisation----------------------------------------
Dict(alist::AList) = Dict("__id__" => "SHIPs_AList",
                           "i2zklm" => zklm2vec.(alist.i2zklm))
AList(D::Dict) = AList(vec2zklm.(D["i2zklm"]))
zklm2vec(t) = [t.z, t.k, t.l, t.m]
vec2zklm(v) = (z=Int16(v[1]), k=IntS(v[2]), l=IntS(v[3]), m=IntS(v[4]))
==(al1::AList, al2::AList) = (al1.i2zklm == al2.i2zklm)
# ------------------------------------------------------------------------

Base.length(alist::AList) = length(alist.i2zklm)
Base.getindex(alist::AList, i::Integer) = alist.i2zklm[i]
Base.getindex(alist::AList, zklm::zklmTuple) = alist.zklm2i[zklm]

alloc_A(alist::AList, T=Float64) = zeros(Complex{T}, length(alist))

function AList(zklmlist::AbstractVector{zklmTuple})
   # sort the tuples - by z, then k, then l, then m
   i2zklm = sort(zklmlist)
   # create the inverse mapping
   zklm2i = Dict{zklmTuple, IntS}()
   for i = 1:length(i2zklm)
      zklm2i[i2zklm[i]] = IntS(i)
   end
   # find the first index for each z
   zmax = maximum( a.z for a in i2zklm )
   firstz = [ findfirst([a.z == iz for a in i2zklm])
              for iz = 1:zmax ]
   return AList( i2zklm, zklm2i, [firstz; length(i2zklm)+1] )
end

"""
This fills the A-array stored in tmp with the A_zklm density projections in
the order specified by AList. It also evaluates the radial and angular basis
functions along the way.
"""
function precompute_A!(A, tmp, alist, Rs, Zs, ship)
   fill!(A, 0)
   for (R, Z) in zip(Rs, Zs)
      # evaluate the r-basis and the R̂-basis for the current neighbour at R
      eval_basis!(tmp.J, tmp.tmpJ, ship.J, norm(R))
      eval_basis!(tmp.Y, tmp.tmpY, ship.SH, R)
      # add the contributions to the A_zklm
      iz = z2i(ship, Z)
      for i = alist.firstz[iz]:(alist.firstz[iz+1]-1)
         zklm = alist[i]
         A[i] += tmp.J[zklm.k+1] * tmp.Y[index_y(zklm.l, zklm.m)]
      end
   end
   return A
end

function precompute_dA!(A, dA, tmp, alist, Rs, Zs, ship)
   fill!(A, 0)
   fill!(dA, zero(eltype(dA)))
   for (iR, (R, Z)) in enumerate(zip(Rs, Zs))
      # precompute the derivatives of the Jacobi polynomials and Ylms
      eval_basis_d!(tmp.J, tmp.dJ, tmp.tmpJ, ship.J, norm(R))
      eval_basis_d!(tmp.Y, tmp.dY, tmp.tmpY, ship.SH, R)
      # deduce the A and dA values
      iz = z2i(ship, Z)
      R̂ = R / norm(R)
      for i = alist.firstz[iz]:(alist.firstz[iz+1]-1)
         zklm = alist[i]
         ik = zklm.k+1; iy = index_y(zklm.l, zklm.m)
         A[i] += tmp.J[ik] * tmp.Y[iy]
         # and into dA # grad_phi_Rj(R, iR, zklm, tmp)
         ∇ϕ_zklm = tmp.dJ[ik] * tmp.Y[iy] * R̂ + tmp.J[ik] * tmp.dY[iy]
         dA[iR, i] = ∇ϕ_zklm
      end
   end
   return dA
end

# ---------

"""
`AAList` : datastructure to help compute the A_𝐳𝐤𝐥𝐦 = ∏ A_zklm

* `i2Aidx` : indices in AList of the zklms to avoid the Dict lookup
* `len`    : len[i] is the number of relevant entries of i2zklm[i,:]
             i.e. the body-order of this entry
* `zklm2i` : dictionary of all (z,k,l,m) tuples to compute  the
             map `(zz,kk,ll,mm) -> i`
* `firstz` : `firstz[iz]` stores the first index in the A_zklm array for with
             z = zi. This can be used to iterate over all A entries for which
             z = zi. (they are sorted by z first)
"""
struct AAList
   i2Aidx::Matrix{IntS}    # where in A can we find these
   len::Vector{IntS}       # body-order
   zklm2i::Dict{Any, IntS} # inverse mapping
end

# --------------(de-)serialisation----------------------------------------

==
function Dict(aalist::AAList)
   ZKLM_list = Vector{Any}(undef, length(aalist))
   for (zzkkllmm, i) in aalist.zklm2i
      ZKLM_list[i] = zzkkllmm
   end
   return Dict("__id__" => "SHIPs_AAList", "ZKLM_list" => ZKLM_list)
end
zzkkllmm2vec(zzkkllmm) = Vector.([zzkkllmm...])
vec2zzkkllmm(v) = (SVector(Int16.(v[1])...),
                   SVector(IntS.(v[2])...),
                   SVector(IntS.(v[3])...),
                   SVector(IntS.(v[4])...))
AAList(D::Dict, alist) = AAList(vec2zzkkllmm.(D["ZKLM_list"]), alist)
==(aal1::AAList, aal2::AAList) = (aal1.i2Aidx == aal2.i2Aidx)
# ------------------------------------------------------------------------

bodyorder(aalist::AAList) = maximum(aalist.len) + 1

Base.length(aalist::AAList) = length(aalist.len)

Base.getindex(aalist::AAList, t::Tuple) = aalist.zklm2i[t]

alloc_AA(aalist::AAList, T = Float64) = zeros(Complex{T}, length(aalist))

"""
`AAList(ZKLM_list, alist)` : standard AAList constructor,
* `ZKLM_list` : a collection of (izz=?, kk=?, ll=?, mm=?) tuples
* `alist` a compatible `AList`
"""
function AAList(ZKLM_list, alist)
   BO = maximum(ν -> length(ν[1]), ZKLM_list)  # body-order -> size of iAidx

   # create arrays to construct AAList
   iAidx = IntS[]
   len = IntS[]
   zklm2i = Dict{Any, IntS}()

   idx = 0
   for (izz, kk, ll, mm) in ZKLM_list
      # store in the index of the current row in the reverse map
      idx += 1
      zklm2i[(izz, kk, ll, mm)] = idx
      # store the body-order of the current ∏A function
      push!(len, length(ll))

      # fill the row of the i2Aidx matrix
      for α = 1:length(ll)
         zklm = (z=izz[α], k=kk[α], l=ll[α], m=IntS(mm[α]))
         iA = alist[zklm]
         push!(iAidx, iA)
      end
      # fill up the iAidx vector with zeros up to the body-order
      # this will create 0 entries in the matrix after reshaping
      for α = (length(ll)+1):BO
         push!(iAidx, 0)
      end
   end
   return AAList( reshape(iAidx, (BO, idx))', len, zklm2i )
end

function precompute_AA!(AA, A, aalist) # tmp, ship::SHIPBasis{T}, iz0) where {T}
   fill!(AA, 1)
   for i = 1:length(aalist)
      for α = 1:aalist.len[i]
         iA = aalist.i2Aidx[i, α]
         AA[i] *= A[iA]
      end
   end
   return nothing
end

# --------------------------------------------------------
# this section of functions is for computing
#  ∂∏A / ∂R_j

# function grad_phi_Rj(Rj, j, zklm, tmp)
#    ik = zklm.k + 1
#    iy = index_y(zklm.l, zklm.m)
#    return ( tmp.dJJ[j, ik] *  tmp.YY[j, iy] * (Rj/norm(Rj))
#            + tmp.JJ[j, ik] * tmp.dYY[j, iy] )
# end

function grad_AA_Ab(iAA, b, alist, aalist, A)
   g = one(eltype(A))
   for a = 1:aalist.len[iAA]
      if a != b
         iA = aalist.i2Aidx[iAA, a]
         g *= A[iA]
      end
   end
   return g
end

function grad_AAi_Rj(iAA, j, Rj::JVec{T}, izj::Integer,
                     alist, aalist, A, AA, dA, tmp) where {T}
   g = zero(JVec{Complex{T}})
   for b = 1:aalist.len[iAA] # body-order
      # A_{n_b} = A[iA]
      iA = aalist.i2Aidx[iAA, b]
      # daa_dab = ∂(∏A_{n_a}) / ∂A_{n_b}
      daa_dab = grad_AA_Ab(iAA, b, alist, aalist, A)
      zklm = alist[iA] # (zklm corresponding to A_{n_b})
      ∇ϕ_zklm = dA[j, iA] # grad_phi_Rj(Rj, j, zklm, tmp)
      g += daa_dab * ∇ϕ_zklm
   end
   return g
end

function grad_AA_Rj!(tmp, ship, j, Rs, Zs, iz0) where {T}
   for iAA = 1:length(ship.aalists[iz0])
      # g = ∂(∏_a A_a) / ∂Rj
      tmp.dAAj[iz0][iAA] = grad_AAi_Rj(iAA, j, Rs[j], z2i(ship, Zs[j]),
                                       ship.alists[iz0], ship.aalists[iz0],
                                       tmp.A[iz0], tmp.AA[iz0], tmp.dA[iz0],
                                       tmp)
   end
   return tmp.dAAj[iz0]
end

# --------------------------------------------------------

"""
convert the "old" `(NuZ, ZKL)` format into the simpler (zz, kk, ll, mm)
format, and at the same time extract the one-particle basis (z, k, l, m)
"""
function alists_from_bgrps(bgrps::Tuple)
   NZ = length(bgrps)
   zzkkllmm_list = [ Tuple[] for _=1:NZ ]
   zklm_set = [ Set() for _=1:NZ ]
   for iz0 = 1:NZ
      for (izz, kk, ll) in bgrps[iz0], mm in _mrange(ll)
         push!(zzkkllmm_list[iz0], (izz, kk, ll, IntS.(mm)))
         for α = 1:length(ll)
            zklm = (z=izz[α], k=kk[α], l=ll[α], m=IntS(mm[α]))
            push!(zklm_set[iz0], zklm)
         end
      end
   end

   alist =  ntuple(iz0 -> AList([ zklm for zklm in collect(zklm_set[iz0]) ]), NZ)
   aalist = ntuple(iz0 -> AAList(zzkkllmm_list[iz0], alist[iz0]), NZ)
   return alist, aalist
end

# --------------------------------------------------------

using SparseArrays: SparseMatrixCSC

function _my_mul!(C::AbstractVector, A::SparseMatrixCSC, B::AbstractVector)
   A.n == length(B) || throw(DimensionMismatch())
   A.m == length(C) || throw(DimensionMismatch())
   nzv = A.nzval
   rv = A.rowval
   fill!(C, zero(eltype(C)))
   @inbounds for col = 1:A.n
      b = B[col]
      for j = A.colptr[col]:(A.colptr[col + 1] - 1)
         C[rv[j]] += nzv[j] * b
      end
   end
   return C
end


# By default, Julia/LLVM does not use fused multiply-add operations (FMAs).
# Since these FMAs can increase the performance of many numerical algorithms,
# we need to opt-in explicitly.
# See https://ranocha.de/blog/Optimizing_EC_Trixi for further details.
@muladd begin

# TODO: Taal refactor
# - analysis_interval part as PeriodicCallback called after a certain amount of simulation time
"""
    AnalysisCallback(semi; interval=0,
                           save_analysis=false,
                           output_directory="out",
                           analysis_filename="analysis.dat",
                           extra_analysis_errors=Symbol[],
                           extra_analysis_integrals=())

Analyze a numerical solution every `interval` time steps and print the
results to the screen. If `save_analysis`, the results are also saved in
`joinpath(output_directory, analysis_filename)`.

Additional errors can be computed, e.g. by passing `extra_analysis_errors = [:primitive]`.

Further scalar functions `func` in `extra_analysis_integrals` are applied to the numerical
solution and integrated over the computational domain.
See `Trixi.analyze`, `Trixi.pretty_form_utf`, `Trixi.pretty_form_ascii` for further
information on how to create custom analysis quantities.
"""
mutable struct AnalysisCallback{Analyzer, AnalysisIntegrals, InitialStateIntegrals, Cache}
  start_time::Float64
  interval::Int
  save_analysis::Bool
  output_directory::String
  analysis_filename::String
  analyzer::Analyzer
  analysis_errors::Vector{Symbol}
  analysis_integrals::AnalysisIntegrals
  initial_state_integrals::InitialStateIntegrals
  cache::Cache
end

# TODO: Taal bikeshedding, implement a method with less information and the signature
# function Base.show(io::IO, analysis_callback::AnalysisCallback)
# end
function Base.show(io::IO, ::MIME"text/plain", cb::DiscreteCallback{<:Any, <:AnalysisCallback})
  @nospecialize cb # reduce precompilation time

  if get(io, :compact, false)
    show(io, cb)
  else
    analysis_callback = cb.affect!

    setup = Pair{String,Any}[
             "interval" => analysis_callback.interval,
             "analyzer" => analysis_callback.analyzer,
            ]
    for (idx, error) in enumerate(analysis_callback.analysis_errors)
      push!(setup, "│ error " * string(idx) => error)
    end
    for (idx, integral) in enumerate(analysis_callback.analysis_integrals)
      push!(setup, "│ integral " * string(idx) => integral)
    end
    push!(setup, "save analysis to file" => analysis_callback.save_analysis ? "yes" : "no")
    if analysis_callback.save_analysis
      push!(setup, "│ filename" => analysis_callback.analysis_filename)
      push!(setup, "│ output directory" => abspath(normpath(analysis_callback.output_directory)))
    end
    summary_box(io, "AnalysisCallback", setup)
  end
end

function AnalysisCallback(semi::AbstractSemidiscretization; kwargs...)
  mesh, equations, solver, cache = mesh_equations_solver_cache(semi)
  AnalysisCallback(mesh, equations, solver, cache; kwargs...)
end

function AnalysisCallback(mesh, equations::AbstractEquations, solver, cache;
                          interval=0,
                          save_analysis=false,
                          output_directory="out",
                          analysis_filename="analysis.dat",
                          extra_analysis_errors=Symbol[],
                          analysis_errors=union(default_analysis_errors(equations), extra_analysis_errors),
                          extra_analysis_integrals=(),
                          analysis_integrals=union(default_analysis_integrals(equations), extra_analysis_integrals),
                          RealT=real(solver),
                          uEltype=eltype(cache.elements),
                          kwargs...)
  # Decide when the callback is activated.
  # With error-based step size control, some steps can be rejected. Thus,
  #   `integrator.iter >= integrator.destats.naccept`
  #    (total #steps)       (#accepted steps)
  # We need to check the number of accepted steps since callbacks are not
  # activated after a rejected step.
  condition = (u, t, integrator) -> interval > 0 && ( (integrator.destats.naccept % interval == 0 &&
                                                       !(integrator.destats.naccept == 0 && integrator.iter > 0)) ||
                                                     isfinished(integrator))

  analyzer = SolutionAnalyzer(solver; kwargs...)
  cache_analysis = create_cache_analysis(analyzer, mesh, equations, solver, cache, RealT, uEltype)

  analysis_callback = AnalysisCallback(0.0, interval, save_analysis, output_directory, analysis_filename,
                                       analyzer,
                                       analysis_errors, Tuple(analysis_integrals),
                                       SVector(ntuple(_ -> zero(uEltype), Val(nvariables(equations)))),
                                       cache_analysis)

  DiscreteCallback(condition, analysis_callback,
                   save_positions=(false,false),
                   initialize=initialize!)
end

function initialize!(cb::DiscreteCallback{Condition,Affect!}, u_ode, t, integrator) where {Condition, Affect!<:AnalysisCallback}
  semi = integrator.p
  initial_state_integrals = integrate(u_ode, semi)
  _, equations, _, _ = mesh_equations_solver_cache(semi)

  analysis_callback = cb.affect!
  analysis_callback.initial_state_integrals = initial_state_integrals
  @unpack save_analysis, output_directory, analysis_filename, analysis_errors, analysis_integrals = analysis_callback

  if save_analysis && mpi_isroot()
    mkpath(output_directory)

    # write header of output file
    open(joinpath(output_directory, analysis_filename), "w") do io
      @printf(io, "#%-8s", "timestep")
      @printf(io, "  %-14s", "time")
      @printf(io, "  %-14s", "dt")
      if :l2_error in analysis_errors
        for v in varnames(cons2cons, equations)
          @printf(io, "   %-14s", "l2_" * v)
        end
      end
      if :linf_error in analysis_errors
        for v in varnames(cons2cons, equations)
          @printf(io, "   %-14s", "linf_" * v)
        end
      end
      if :conservation_error in analysis_errors
        for v in varnames(cons2cons, equations)
          @printf(io, "   %-14s", "cons_" * v)
        end
      end
      if :residual in analysis_errors
        for v in varnames(cons2cons, equations)
          @printf(io, "   %-14s", "res_" * v)
        end
      end
      if :l2_error_primitive in analysis_errors
        for v in varnames(cons2prim, equations)
          @printf(io, "   %-14s", "l2_" * v)
        end
      end
      if :linf_error_primitive in analysis_errors
        for v in varnames(cons2prim, equations)
          @printf(io, "   %-14s", "linf_" * v)
        end
      end

      for quantity in analysis_integrals
        @printf(io, "   %-14s", pretty_form_ascii(quantity))
      end

      println(io)
    end

  end

  analysis_callback.start_time = time_ns()
  analysis_callback(integrator)
  return nothing
end

# TODO: Taal refactor, allow passing an IO object (which could be devnull to avoid cluttering the console)
function (analysis_callback::AnalysisCallback)(integrator)
  semi = integrator.p
  mesh, equations, solver, cache = mesh_equations_solver_cache(semi)
  @unpack dt, t = integrator
  iter = integrator.destats.naccept

  runtime_absolute = 1.0e-9 * (time_ns() - analysis_callback.start_time)
  runtime_relative = 1.0e-9 * take!(semi.performance_counter) / ndofs(semi)

  @trixi_timeit timer() "analyze solution" begin
    # General information
    mpi_println()
    mpi_println("─"^100)
    # TODO: Taal refactor, polydeg is specific to DGSEM
    mpi_println(" Simulation running '", get_name(equations), "' with ", summary(solver))
    mpi_println("─"^100)
    mpi_println(" #timesteps:     " * @sprintf("% 14d", iter) *
                "               " *
                " run time:       " * @sprintf("%10.8e s", runtime_absolute))
    mpi_println(" Δt:             " * @sprintf("%10.8e", dt) *
                "               " *
                " time/DOF/rhs!:  " * @sprintf("%10.8e s", runtime_relative))
    mpi_println(" sim. time:      " * @sprintf("%10.8e", t))
    mpi_println(" #DOF:           " * @sprintf("% 14d", ndofs(semi)))
    mpi_println(" #elements:      " * @sprintf("% 14d", nelements(mesh, solver, cache)))

    # Level information (only show for AMR)
    print_amr_information(integrator.opts.callback, mesh, solver, cache)
    mpi_println()

    # Open file for appending and store time step and time information
    if mpi_isroot() && analysis_callback.save_analysis
      io = open(joinpath(analysis_callback.output_directory, analysis_callback.analysis_filename), "a")
      @printf(io, "% 9d", iter)
      @printf(io, "  %10.8e", t)
      @printf(io, "  %10.8e", dt)
    else
      io = devnull
    end

    # Calculate current time derivative (needed for semidiscrete entropy time derivative, residual, etc.)
    du_ode = first(get_tmp_cache(integrator))
    @notimeit timer() rhs!(du_ode, integrator.u, semi, t)
    u  = wrap_array(integrator.u, mesh, equations, solver, cache)
    du = wrap_array(du_ode,       mesh, equations, solver, cache)
    l2_error, linf_error = analysis_callback(io, du, u, integrator.u, t, semi)

    mpi_println("─"^100)
    mpi_println()

    # Add line break and close analysis file if it was opened
    if mpi_isroot() && analysis_callback.save_analysis
      # This resolves a possible type instability introduced above, since `io`
      # can either be an `IOStream` or `devnull`, but we know that it must be
      # an `IOStream here`.
      println(io::IOStream)
      close(io::IOStream)
    end
  end

  # avoid re-evaluating possible FSAL stages
  u_modified!(integrator, false)

  # Return errors for EOC analysis
  return l2_error, linf_error
end

# This method is just called internally from `(analysis_callback::AnalysisCallback)(integrator)`
# and serves as a function barrier. Additionally, it makes the code easier to profile and optimize.
function (analysis_callback::AnalysisCallback)(io, du, u, u_ode, t, semi)
  @unpack analyzer, analysis_errors, analysis_integrals = analysis_callback
  cache_analysis = analysis_callback.cache
  _, equations, _, _ = mesh_equations_solver_cache(semi)

  # Calculate and print derived quantities (error norms, entropy etc.)
  # Variable names required for L2 error, Linf error, and conservation error
  if any(q in analysis_errors for q in
         (:l2_error, :linf_error, :conservation_error, :residual)) && mpi_isroot()
    print(" Variable:    ")
    for v in eachvariable(equations)
      @printf("   %-14s", varnames(cons2cons, equations)[v])
    end
    println()
  end

  # Calculate L2/Linf errors, which are also returned
  l2_error, linf_error = calc_error_norms(u_ode, t, analyzer, semi, cache_analysis)

  if mpi_isroot()
    # L2 error
    if :l2_error in analysis_errors
      print(" L2 error:    ")
      for v in eachvariable(equations)
        @printf("  % 10.8e", l2_error[v])
        @printf(io, "  % 10.8e", l2_error[v])
      end
      println()
    end

    # Linf error
    if :linf_error in analysis_errors
      print(" Linf error:  ")
      for v in eachvariable(equations)
        @printf("  % 10.8e", linf_error[v])
        @printf(io, "  % 10.8e", linf_error[v])
      end
      println()
    end
  end

  # Conservation errror
  if :conservation_error in analysis_errors
    @unpack initial_state_integrals = analysis_callback
    state_integrals = integrate(u_ode, semi)

    if mpi_isroot()
      print(" |∑U - ∑U₀|:  ")
      for v in eachvariable(equations)
        err = abs(state_integrals[v] - initial_state_integrals[v])
        @printf("  % 10.8e", err)
        @printf(io, "  % 10.8e", err)
      end
      println()
    end
  end

  # Residual (defined here as the vector maximum of the absolute values of the time derivatives)
  if :residual in analysis_errors
    mpi_print(" max(|Uₜ|):   ")
    for v in eachvariable(equations)
      # Calculate maximum absolute value of Uₜ
      res = maximum(abs, view(du, v, ..))
      if mpi_isparallel()
        # TODO: Debugging, here is a type instability
        global_res = MPI.Reduce!(Ref(res), max, mpi_root(), mpi_comm())
        if mpi_isroot()
          res::eltype(du) = global_res[]
        end
      end
      if mpi_isroot()
        @printf("  % 10.8e", res)
        @printf(io, "  % 10.8e", res)
      end
    end
    mpi_println()
  end

  # L2/L∞ errors of the primitive variables
  if :l2_error_primitive in analysis_errors || :linf_error_primitive in analysis_errors
    l2_error_prim, linf_error_prim = calc_error_norms(cons2prim, u_ode, t, analyzer, semi, cache_analysis)

    if mpi_isroot()
      print(" Variable:    ")
      for v in eachvariable(equations)
        @printf("   %-14s", varnames(cons2prim, equations)[v])
      end
      println()

      # L2 error
      if :l2_error_primitive in analysis_errors
        print(" L2 error prim.: ")
        for v in eachvariable(equations)
          @printf("%10.8e   ", l2_error_prim[v])
          @printf(io, "  % 10.8e", l2_error_prim[v])
        end
        println()
      end

      # L∞ error
      if :linf_error_primitive in analysis_errors
        print(" Linf error pri.:")
        for v in eachvariable(equations)
          @printf("%10.8e   ", linf_error_prim[v])
          @printf(io, "  % 10.8e", linf_error_prim[v])
        end
        println()
      end
    end
  end

  # additional integrals
  analyze_integrals(analysis_integrals, io, du, u, t, semi)

  return l2_error, linf_error
end

# Print level information only if AMR is enabled
function print_amr_information(callbacks, mesh, solver, cache)

  # Return early if there is nothing to print
  uses_amr(callbacks) || return nothing

  levels = Vector{Int}(undef, nelements(solver, cache))
  min_level = typemax(Int)
  max_level = typemin(Int)
  for element in eachelement(solver, cache)
    current_level = mesh.tree.levels[cache.elements.cell_ids[element]]
    levels[element] = current_level
    min_level = min(min_level, current_level)
    max_level = max(max_level, current_level)
  end

  for level = max_level:-1:min_level+1
    mpi_println(" ├── level $level:    " * @sprintf("% 14d", count(==(level), levels)))
  end
  mpi_println(" └── level $min_level:    " * @sprintf("% 14d", count(==(min_level), levels)))

  return nothing
end

# Print level information only if AMR is enabled
function print_amr_information(callbacks, mesh::P4estMesh, solver, cache)

  # Return early if there is nothing to print
  uses_amr(callbacks) || return nothing

  elements_per_level = zeros(P4EST_MAXLEVEL + 1)

  for tree in unsafe_wrap_sc(p4est_tree_t, mesh.p4est.trees)
    elements_per_level .+= tree.quadrants_per_level
  end

  min_level = findfirst(i -> i > 0, elements_per_level) - 1
  max_level = findlast(i -> i > 0, elements_per_level) - 1

  for level = max_level:-1:min_level+1
    mpi_println(" ├── level $level:    " * @sprintf("% 14d", elements_per_level[level + 1]))
  end
  mpi_println(" └── level $min_level:    " * @sprintf("% 14d", elements_per_level[min_level + 1]))

  return nothing
end

# Iterate over tuples of analysis integrals in a type-stable way using "lispy tuple programming".
function analyze_integrals(analysis_integrals::NTuple{N,Any}, io, du, u, t, semi) where {N}

  # Extract the first analysis integral and process it; keep the remaining to be processed later
  quantity = first(analysis_integrals)
  remaining_quantities = Base.tail(analysis_integrals)

  res = analyze(quantity, du, u, t, semi)
  if mpi_isroot()
    @printf(" %-12s:", pretty_form_utf(quantity))
    @printf("  % 10.8e", res)
    @printf(io, "  % 10.8e", res)
  end
  mpi_println()

  # Recursively call this method with the unprocessed integrals
  analyze_integrals(remaining_quantities, io, du, u, t, semi)
  return nothing
end

# terminate the type-stable iteration over tuples
function analyze_integrals(analysis_integrals::Tuple{}, io, du, u, t, semi)
  nothing
end

# used for error checks and EOC analysis
function (cb::DiscreteCallback{Condition,Affect!})(sol) where {Condition, Affect!<:AnalysisCallback}
  analysis_callback = cb.affect!
  semi = sol.prob.p
  @unpack analyzer = analysis_callback
  cache_analysis = analysis_callback.cache

  l2_error, linf_error = calc_error_norms(sol.u[end], sol.t[end], analyzer, semi, cache_analysis)
  (; l2=l2_error, linf=linf_error)
end

# some common analysis_integrals
# to support another analysis integral, you can overload
# Trixi.analyze, Trixi.pretty_form_utf, Trixi.pretty_form_ascii
function analyze(quantity, du, u, t, semi::AbstractSemidiscretization)
  mesh, equations, solver, cache = mesh_equations_solver_cache(semi)
  analyze(quantity, du, u, t, mesh, equations, solver, cache)
end
function analyze(quantity, du, u, t, mesh, equations, solver, cache)
  integrate(quantity, u, mesh, equations, solver, cache, normalize=true)
end
pretty_form_utf(quantity) = get_name(quantity)
pretty_form_ascii(quantity) = get_name(quantity)

function entropy_timederivative end
pretty_form_utf(::typeof(entropy_timederivative)) = "∑∂S/∂U ⋅ Uₜ"
pretty_form_ascii(::typeof(entropy_timederivative)) = "dsdu_ut"

pretty_form_utf(::typeof(entropy)) = "∑S"

pretty_form_utf(::typeof(energy_total)) = "∑e_total"
pretty_form_ascii(::typeof(energy_total)) = "e_total"

pretty_form_utf(::typeof(energy_kinetic)) = "∑e_kinetic"
pretty_form_ascii(::typeof(energy_kinetic)) = "e_kinetic"

pretty_form_utf(::typeof(energy_kinetic_nondimensional)) = "∑e_kinetic*"
pretty_form_ascii(::typeof(energy_kinetic_nondimensional)) = "e_kinetic*"

pretty_form_utf(::typeof(energy_internal)) = "∑e_internal"
pretty_form_ascii(::typeof(energy_internal)) = "e_internal"

pretty_form_utf(::typeof(energy_magnetic)) = "∑e_magnetic"
pretty_form_ascii(::typeof(energy_magnetic)) = "e_magnetic"

pretty_form_utf(::typeof(cross_helicity)) = "∑v⋅B"
pretty_form_ascii(::typeof(cross_helicity)) = "v_dot_B"

pretty_form_utf(::Val{:l2_divb}) = "L2 ∇⋅B"
pretty_form_ascii(::Val{:l2_divb}) = "l2_divb"

pretty_form_utf(::Val{:linf_divb}) = "L∞ ∇⋅B"
pretty_form_ascii(::Val{:linf_divb}) = "linf_divb"

pretty_form_utf(::typeof(lake_at_rest_error)) = "∑|H₀-(h+b)|"
pretty_form_ascii(::typeof(lake_at_rest_error)) = "|H0-(h+b)|"

# specialized implementations specific to some solvers
include("analysis_dg1d.jl")
include("analysis_dg2d.jl")
include("analysis_dg2d_parallel.jl")
include("analysis_dg3d.jl")
include("analysis_dg3d_parallel.jl")

end # @muladd


thermal_model = DeviceModel(PSY.ThermalStandard, PSI.ThermalDispatch)
load_model = DeviceModel(PSY.PowerLoad, PSI.StaticPowerLoad)
line_model = DeviceModel(PSY.Line, PSI.StaticLine)
transformer_model = DeviceModel(PSY.Transformer2W, PSI.StaticTransformer)
ttransformer_model = DeviceModel(PSY.TapTransformer, PSI.StaticTransformer)
dc_line = DeviceModel(PSY.HVDCLine, PSI.HVDCDispatch)

@testset "Network Copper Plate" begin
    network = CopperPlatePowerModel
    systems = [c_sys5, c_sys14, c_sys14_dc]
    parameters = [true, false]
    test_results = Dict{PSY.System, Vector{Int64}}(c_sys5 => [120, 120, 0, 0, 24],
                                                   c_sys14 => [120, 120, 0, 0, 24],
                                                   c_sys14_dc => [120, 120, 0, 0, 24])
    constraint_names = [:CopperPlateBalance]
    objfuncs = [GAEVF, GQEVF, GQEVF]

    for (ix, sys) in enumerate(systems), p in parameters
        ps_model = OperationModel(TestOptModel, network, sys; optimizer = OSQP_optimizer, parameters = p)
        construct_device!(ps_model, :Thermal, thermal_model; parameters = p);
        construct_device!(ps_model, :Load, load_model; parameters = p);
        construct_network!(ps_model, network; parameters = p);
        construct_device!(ps_model, :Line, line_model; parameters = p);
        construct_device!(ps_model, :Tf, transformer_model; parameters = p);
        construct_device!(ps_model, :TTf, ttransformer_model; parameters = p);
        construct_device!(ps_model, :DCLine, dc_line; parameters = p);

        moi_tests(ps_model, p, test_results[sys][1],
                                test_results[sys][2],
                                test_results[sys][3],
                                test_results[sys][4],
                                test_results[sys][5], false)
        psi_constraint_test(ps_model, constraint_names)
        psi_checkobjfun_test(ps_model, objfuncs[ix])
        psi_checksolve_test(ps_model, [MOI.OPTIMAL])
    end
end

@testset "Network DC-PF with PTDF formulation" begin
    network = StandardPTDFModel
    systems = [c_sys5, c_sys14, c_sys14_dc]
    objfuncs = [GAEVF, GQEVF, GQEVF]
    constraint_names = [:RateLimit_Line, :nodal_balance, :network_flow]
    parameters = [true, false]
    PTDF_ref = Dict{PSY.System, PSY.PTDF}(c_sys5 => PTDF5, c_sys14 => PTDF14, c_sys14_dc => PTDF14_dc);
    test_results = Dict{PSY.System, Vector{Int64}}(c_sys5 => [264, 264, 0, 0, 264],
                                                    c_sys14 => [600, 600, 0, 0, 816],
                                                    c_sys14_dc => [600, 600, 0, 0, 768])

    for (ix, sys) in enumerate(systems), p in parameters
        ps_model = OperationModel(TestOptModel, network, sys; optimizer = OSQP_optimizer, parameters = p)
        construct_device!(ps_model, :Thermal, thermal_model; parameters = p);
        construct_device!(ps_model, :Load, load_model; parameters = p);
        construct_network!(ps_model, network; PTDF = PTDF_ref[sys], parameters = p);
        construct_device!(ps_model, :Line, line_model; parameters = p);
        construct_device!(ps_model, :Tf, transformer_model; parameters = p);
        construct_device!(ps_model, :TTf, ttransformer_model; parameters = p);
        construct_device!(ps_model, :DCLine, dc_line; parameters = p);

        moi_tests(ps_model, p, test_results[sys][1],
                                test_results[sys][2],
                                test_results[sys][3],
                                test_results[sys][4],
                                test_results[sys][5], false)
        psi_constraint_test(ps_model, constraint_names)
        psi_checkobjfun_test(ps_model, objfuncs[ix])
        psi_checksolve_test(ps_model, [MOI.OPTIMAL])
    end

    #PTDF input Error testing
    ps_model = OperationModel(TestOptModel, network, c_sys5; optimizer = GLPK_optimizer)
    construct_device!(ps_model, :Thermal, thermal_model);
    construct_device!(ps_model, :Load, load_model);
    @test_throws ArgumentError construct_network!(ps_model, network)

end

@testset "Network DC lossless -PF network with PowerModels DCPlosslessForm" begin
    network = DCPPowerModel
    systems = [c_sys5, c_sys14, c_sys14_dc]
    objfuncs = [GAEVF, GQEVF, GQEVF]
    constraint_names = [:RateLimit_Line]
    parameters = [true, false]
    test_results = Dict{PSY.System, Vector{Int64}}(c_sys5 => [384, 264, 144, 144, 288],
                                                    c_sys14 => [936, 600, 480, 480, 840],
                                                    c_sys14_dc => [984, 600, 432, 432, 840])

    for (ix, sys) in enumerate(systems), p in parameters
        ps_model = OperationModel(TestOptModel, network, sys; optimizer = OSQP_optimizer, parameters = p)
        construct_device!(ps_model, :Thermal, thermal_model; parameters = p);
        construct_device!(ps_model, :Load, load_model; parameters = p);
        construct_network!(ps_model, network; parameters = p);
        construct_device!(ps_model, :Line, line_model; parameters = p);
        construct_device!(ps_model, :Tf, transformer_model; parameters = p);
        construct_device!(ps_model, :TTf, ttransformer_model; parameters = p);
        construct_device!(ps_model, :DCLine, dc_line; parameters = p);

        moi_tests(ps_model, p, test_results[sys][1],
                                test_results[sys][2],
                                test_results[sys][3],
                                test_results[sys][4],
                                test_results[sys][5], false)
        psi_constraint_test(ps_model, constraint_names)
        psi_checkobjfun_test(ps_model, objfuncs[ix])
        psi_checksolve_test(ps_model, [MOI.OPTIMAL])
    end

end

@testset  "Network Solve AC-PF PowerModels StandardACPModel" begin
    network = ACPPowerModel
    systems = [c_sys5, c_sys14, c_sys14_dc]
    objfuncs = [GAEVF, GQEVF, GQEVF]
    constraint_names = [:RateLimitFT_Line, :RateLimitTF_Line]
    parameters = [true, false]
    test_results = Dict{PSY.System, Vector{Int64}}(c_sys5 => [1056, 240, 144, 144, 264],
                                                    c_sys14 => [2832, 240, 480, 480, 696],
                                                    c_sys14_dc => [2832, 336, 432, 432, 744]) # TODO: changed the interval constraint number to 336 from 240. double check

    for (ix, sys) in enumerate(systems), p in parameters
        ps_model = OperationModel(TestOptModel, network, sys; optimizer = ipopt_optimizer, parameters = p)
        construct_device!(ps_model, :Thermal, thermal_model; parameters = p);
        construct_device!(ps_model, :Load, load_model; parameters = p);
        construct_network!(ps_model, network; parameters = p);
        construct_device!(ps_model, :Line, line_model; parameters = p);
        construct_device!(ps_model, :Tf, transformer_model; parameters = p);
        construct_device!(ps_model, :TTf, ttransformer_model; parameters = p);
        construct_device!(ps_model, :DCLine, dc_line; parameters = p);

        moi_tests(ps_model, p, test_results[sys][1],
                                test_results[sys][2],
                                test_results[sys][3],
                                test_results[sys][4],
                                test_results[sys][5], false)
        psi_constraint_test(ps_model, constraint_names)
        psi_checkobjfun_test(ps_model, objfuncs[ix])
        psi_checksolve_test(ps_model, [MOI.OPTIMAL, MOI.LOCALLY_SOLVED])
    end
end

@testset  "Network Solve AC-PF PowerModels linear approximation models" begin
    networks = [DCPPowerModel, NFAPowerModel]
    systems = [c_sys5, c_sys14, c_sys14_dc]
    p = true
    for network in networks, sys in systems
        @info "Testing construction of a $(network) network"
        ps_model = OperationModel(TestOptModel, network, sys; optimizer = OSQP_optimizer, parameters = p)
        construct_device!(ps_model, :Thermal, thermal_model; parameters = p);
        construct_device!(ps_model, :Load, load_model; parameters = p);
        construct_network!(ps_model, network; parameters = p);
        construct_device!(ps_model, :Line, line_model; parameters = p);
        construct_device!(ps_model, :DCLine, dc_line; parameters = p);
        psi_checksolve_test(ps_model, [MOI.OPTIMAL])

    end

end

@testset  "Network AC-PF PowerModels non-convex models" begin
    networks = [#ACPPowerModel, Already tested
                ACRPowerModel,
                ACTPowerModel
                ]
    systems = [c_sys5, c_sys14, c_sys14_dc]

    for network in networks, sys in systems
        @info "Testing construction of a $(network) network"
        ps_model = OperationModel(TestOptModel, network, sys; optimizer = ipopt_optimizer)
        construct_device!(ps_model, :Thermal, thermal_model);
        construct_device!(ps_model, :Load, load_model);
        construct_network!(ps_model, network);
        construct_device!(ps_model, :Line, line_model);
        @test !isnothing(ps_model.canonical.pm_model)
    end

end

@testset  "Network AC-PF PowerModels quadratic loss approximations models" begin
    networks = [DCPLLPowerModel, LPACCPowerModel]
    systems = [c_sys5, c_sys14, c_sys14_dc]

    for network in networks, sys in systems
        @info "Testing construction of a $(network) network"
        ps_model = OperationModel(TestOptModel, network, sys; optimizer = ipopt_optimizer)
        construct_device!(ps_model, :Thermal, thermal_model);
        construct_device!(ps_model, :Load, load_model);
        construct_network!(ps_model, network);
        construct_device!(ps_model, :Line, line_model);
        construct_device!(ps_model, :DCLine, dc_line);
        @test !isnothing(ps_model.canonical.pm_model)
    end

end

@testset  "Network AC-PF PowerModels quadratic relaxations models" begin
    networks = [ SOCWRPowerModel,
                 QCRMPowerModel,
                 QCLSPowerModel,
                 ]
    systems = [c_sys5, c_sys14, c_sys14_dc]

    for network in networks, sys in systems
        @info "Testing construction of a $(network) network"
        ps_model = OperationModel(TestOptModel, network, sys; optimizer = ipopt_optimizer)
        construct_device!(ps_model, :Thermal, thermal_model);
        construct_device!(ps_model, :Load, load_model);
        construct_network!(ps_model, network);
        construct_device!(ps_model, :Line, line_model);
        construct_device!(ps_model, :DCLine, dc_line);
        @test !isnothing(ps_model.canonical.pm_model)
    end

end

@testset  "Network Unsupported Power Model Formulations" begin
incompat_list = [PM.SDPWRMPowerModel,
                 PM.SparseSDPWRMPowerModel,
                 PM.SOCBFPowerModel,
                 PM.SOCBFConicPowerModel]

    for network in incompat_list
        ps_model = OperationModel(TestOptModel, network, c_sys5; optimizer = ipopt_optimizer)
        construct_device!(ps_model, :Thermal, thermal_model);
        construct_device!(ps_model, :Load, load_model);

        @test_throws ArgumentError construct_network!(ps_model, network);
    end

end


#=============================#
#== put needed modules here ==#
#=============================#
import JuliaChem

#================================#
#== JuliaChem execution script ==#
#================================#
Base.@ccallable function julia_main(ARGS::Vector{String})::Cint
  #== initialize JuliaChem runtime ==#
  JuliaChem.initialize()

  #== read in input file ==#
  input_file = ARGS[1]
  molecule, driver, model, keywords = JuliaChem.JCInput.run(input_file)

  #== generate basis set ==#
  basis = JuliaChem.JCBasis.run(molecule, model)

  #== perform scf calculation ==#
  if (driver == "energy")
    if (model["method"] == "RHF")
      scf = JuliaChem.JCRHF.run(basis, molecule, keywords)
    end
  end

  #== finalize JuliaChem runtime ==#
  JuliaChem.finalize()

  #== we are done ==#
  return 0
end


__precompile__()

"""
Functions to learn a metamotif model of nucleosome positions by nested sampling
"""

module nnlearn
    using BenchmarkTools, BioSequences, DataFrames, Distributed, Distributions, ProgressMeter, Serialization, UnicodePlots, JLD2
    import ProgressMeter: AbstractProgress
    import Printf: @sprintf
    import StatsFuns: logaddexp, logsumexp #both are needed as logsumexp for two terms is deprecated
    import Random: rand, seed!, shuffle!
    import Distances: euclidean

    mutable struct Model_Record #record struct to associate a log_Li with a saved, calculated model
        path::String
        log_Li::AbstractFloat
    end

    function read_fa_wms_tr(path::String)
        wms=Vector{Matrix{AbstractFloat}}()
        wm=zeros(1,4)
        f=open(path)
        for line in eachline(f)
            prefix=line[1:2]
            prefix == "01" && (wm=transpose([parse(AbstractFloat,i) for i in split(line)[2:end]]))
            prefix != "01" && prefix != "NA" && prefix != "PO" && prefix != "//" && (wm=vcat(wm, transpose([parse(AbstractFloat,i) for i in split(line)[2:end]])))
            prefix == "//" && push!(wms, wm)
        end
        return wms
    end

    #wm_samples are in decimal probability space, not log space
    function assemble_source_priors(no_sources::Integer, wm_samples::Vector{Matrix{AbstractFloat}}, prior_wt::AbstractFloat, uninform_length_range::UnitRange{Integer}, unspecified_wm_prior=Dirichlet(ones(4)/4)) #estimate a dirichlet prior on wm_samples inputs; unless otherwise specified, fill non-wm_samples-specified priors with uninformative priors of random lengths in the specified range
        source_priors = Vector{Vector{Dirichlet{AbstractFloat}}}()
        for source in 1:no_sources
            if source <= length(wm_samples)
                push!(source_priors, estimate_dirichlet_prior_on_wm(wm_samples[source], prior_wt))
            else
                uninformative_prior = Vector{Dirichlet{AbstractFloat}}()
                for pos in 1:rand(uninform_length_range)
                    push!(uninformative_prior, unspecified_wm_prior)
                end
                push!(source_priors, uninformative_prior)
            end
        end
        return source_priors
    end

                function estimate_dirichlet_prior_on_wm(wm::Matrix{AbstractFloat}, wt::AbstractFloat)
                    for i in 1:size(wm)[1]
                        !(isprobvec(wm[i,:])) && throw(DomainError("Bad weight vec supplied to estimate_dirichlet_prior_on_wm! $(wm[i,:])"))
                    end
                    prior = Vector{Dirichlet{AbstractFloat}}()
                    for position in 1:size(wm)[1]
                        normvec=wm[position,:]
                        zero_idxs=findall(isequal(0.),wm[position,:])
                        normvec[zero_idxs].+=10^-99
                        push!(prior, Dirichlet(normvec.*wt))
                    end
                    return prior
                end

    function cluster_mix_prior!(df::DataFrame, wms::Vector{Matrix{AbstractFloat}})
        mix=falses(size(df,1),length(wms))
        for (o, row) in enumerate(eachrow(df))
            row.cluster != 0 && (mix[o,row.cluster]=true)
        end

        represented_sources=unique(df.cluster)
        wms=wms[represented_sources]
        return mix[:,represented_sources]
    end

    #subfuncs to handle sums of log probabilities that may include -Inf (ie p=0), returning -Inf in this case rather than NaNs
    function lps(adjuvants::AbstractArray)
        prob = sum(adjuvants) ; isnan(prob) ? - Inf : prob
    end

    function lps(base, adjuvants...)::AbstractFloat
        prob = base+sum(adjuvants) ; isnan(prob) ? -Inf : prob
    end

    function infocenter_wms_trim(wm::Matrix{AbstractFloat}, trimsize::Integer)
        !(size(wm,2)==4) && throw(DomainError("Bad wm! 2nd dimension should be size 4"))
        infovec=get_pwm_info(wm, logsw=false)
        maxval, maxidx=findmax(infovec)
        upstream_extension=Int(floor((trimsize-1)/2))
        downstream_extension=Int(ceil((trimsize-1)/2))
        1+upstream_extension+downstream_extension > size(wm,1) && throw(DomainError("Src too short for trim! $upstream_extension $downstream_extension"))
        return wm[max(1,maxidx-upstream_extension):min(maxidx+downstream_extension,size(wm,1)),:]
    end

    function filter_priors(target_src_no::Integer, target_src_size::Integer, prior_wms::Vector{Matrix{AbstractFloat}}, prior_mix::BitMatrix)
        wms=Vector{Matrix{AbstractFloat}}(undef, target_src_no)
        freqsort_idxs=sortperm([sum(prior_mix[:,s]) for s in 1:length(prior_wms)])
        for i in 1:target_src_no
            target_src_idx=freqsort_idxs[i]
            wms[i]=infocenter_wms_trim(prior_wms[target_src_idx], target_src_size)
        end
        return wms
    end

    function combine_filter_priors(target_src_no::Integer, target_src_size::Integer, prior_wms::Tuple{Vector{Matrix{AbstractFloat}},Vector{Matrix{AbstractFloat}}}, prior_mix::Tuple{BitMatrix,BitMatrix})
        wms=Vector{Matrix{AbstractFloat}}(undef, target_src_no)
        cat_wms=vcat(prior_wms[1],prior_wms[2])
        first_freq=[sum(prior_mix[1][:,s]) for s in 1:length(prior_wms[1])]
        second_freq=[sum(prior_mix[2][:,s]) for s in 1:length(prior_wms[2])]
        freqsort_idxs=sortperm(vcat(first_freq,second_freq))
        for i in 1:target_src_no
            target_src_idx=freqsort_idxs[i]
            wms[i]=infocenter_wms_trim(cat_wms[target_src_idx], target_src_size)
        end
        return wms
    end

    include("ICA_PWM_model.jl")
    include("Bayes_IPM_ensemble.jl")
    include("nested_sampler.jl")
    include("model_display.jl")
    include("ns_progressmeter.jl")
    include("ensemble_utils.jl")
    include("permute_control.jl")
    include("permute_instructions.jl")

    #include("performance.jl")
end # module


@testset "can_avx" begin

    @test LoopVectorization.ArrayInterface.can_avx(log)
    @test LoopVectorization.ArrayInterface.can_avx(log1p)
    @test LoopVectorization.ArrayInterface.can_avx(exp)
    @test LoopVectorization.ArrayInterface.can_avx(+)
    @test LoopVectorization.ArrayInterface.can_avx(-)
    @test LoopVectorization.ArrayInterface.can_avx(Base.FastMath.add_fast)
    @test LoopVectorization.ArrayInterface.can_avx(/)
    @test LoopVectorization.ArrayInterface.can_avx(sqrt)
    @test !LoopVectorization.ArrayInterface.can_avx(clenshaw)

end