using Colors, ColorVectorSpace, StaticArrays
using GeometryBasics, Interpolations
using ImageView
using Makie: orthographicprojection

@inline function edge_function(a, b, c)
    (c[1] - a[1]) * (b[2] - a[2]) - (c[2] - a[2]) * (b[1] - a[1])
end

@inline function src_alpha(c::T) where T <: Colorant
    a = alpha(c)
    a == 0.0 && return zero(T)
    c ./ a
end

one_minus_alpha(c::T) where {T <: Colorant} = one(T) .- src_alpha(c)
blend(source, dest, src_func, dest_func) = clamp01(src_func(source) .+ dest_func(dest))
ColorTypes.alpha(x::StaticVector) = x[4]

function standard_transparency(source, dest::T) where T
    (alpha(source) .* source) .+ ((one(eltype(T)) - alpha(source)) .* dest)
end

mutable struct FixedGeomView{GeomOut, VT}
    buffer::Vector{GeomOut}
    view::VT
    idx::Int
end

struct TriangleStripView{T} <: AbstractVector{NTuple{3, T}}
    parent::AbstractVector{T}
end

Base.length(x::TriangleStripView) = 2
Base.size(x::TriangleStripView) = (2,)

function Base.getindex(x::TriangleStripView, i)
    if i === 1
        return ntuple(i-> x.parent[i], 3)
    elseif i === 2
        return map(i-> x.parent[i], (3, 2, 4))
    else
        error("Out of bounds")
    end
end

function FixedGeomView(T, max_primitives, primitive_in, primitive_out)
    buffer = Vector{Tuple{Point4f, T}}(undef, max_primitives)
    # TODO implement primitive_in and out correctly
    # this is for triangle_strip and 4 max_primitives
    if max_primitives != 4 || primitive_out != :triangle_strip
        error("Not implemented for max_primitives == $max_primitives and primitive_out == $primitive_out.")
    end
    geometry_view = if primitive_out == :triangle_strip
        TriangleStripView(buffer)
    else
        error("$primitive_out not supported. Only :triangle_strip supported right now")
    end
    return FixedGeomView(buffer, geometry_view, 1)
end

function reset!(A::FixedGeomView)
    A.idx = 1
end

function Base.push!(A::FixedGeomView, element)
    if A.idx > length(A.buffer)
        error("Emit called more often than max_primitives. max_primitives: $(length(A.buffer))")
    end
    A.buffer[A.idx] = element
    A.idx += 1
    return
end

struct JLRasterizer{Vertex, Args, FragN, VS, FS, GS, GV, EF}
    vertexshader::VS
    fragmentshader::FS

    geometryshader::GS
    geometry_view::GV
    emit::EF
end

function JLRasterizer{Vertex, Args, FragN}(
        vertexshader::VS,
        fragmentshader::FS,
        geometryshader::GS,
        geometry_view::GV,
        emit::EF
    ) where {Vertex, Args, FragN, VS, FS, GS, GV, EF}
    JLRasterizer{Vertex, Args, FragN, VS, FS, GS, GV, EF}(
        vertexshader,
        fragmentshader,
        geometryshader,
        geometry_view,
        emit
    )
end

function geometry_return_type(vertex_array, vertexshader, geometryshader, uniforms)
    typ = Any
    emit_t(position, ::T) where {T} = (typ = T)
    face1 = first(vertex_array)
    vertex_stage = map(reverse(face1)) do f
        vertexshader(f, uniforms...)
    end
    geometryshader(emit_t, vertex_stage, uniforms...) # figure out typ
    typ
end

arglength(::Type{T}) where {T <: Tuple} = length(T.parameters)
arglength(::Type{T}) where {T <: AbstractArray} = 1
arglength(::Type{T}) where {T} = nfields(T)

function rasterizer(
        vertexarray::AbstractArray,
        uniforms::Tuple,
        vertexshader::Function,
        fragmentshader::Function;
        geometryshader = nothing,
        max_primitives = 4,
        primitive_in = :points,
        primitive_out = :triangle_strip,
    )

    emit, geometry_view = nothing, nothing
    fragment_in_ndim = if !isnothing(geometryshader)
        T = geometry_return_type(vertexarray, vertexshader, geometryshader, uniforms)
        geometry_view = FixedGeomView(T, max_primitives, primitive_in, primitive_out)
        emit = (position, fragment_args) -> push!(geometry_view, (position, fragment_args))
        arglength(T)
    else
        # when we don't have a geometry shader, vertex shader will feed fragment shader
        T = Base.Core.Inference.return_type(vertexshader, Tuple{eltype(vertexarray), map(typeof, uniforms)...})
        if T <: Tuple
            # TODO error handling
            arglength(T.parameters[2])
        else # if not a tuple, vertex shader doesn't pass any arguments to fragment shader
            0
        end
    end

    raster = JLRasterizer{eltype(vertexarray), typeof(uniforms), fragment_in_ndim}(
        vertexshader,
        fragmentshader,
        geometryshader,
        geometry_view,
        emit
    )
    raster, (vertexarray, uniforms)
end

Base.@pure Next(::Val{N}) where {N} = Val(N - 1)

function interpolate(bary, face::NTuple{N, T}, vn::Val{0}, aggregate) where {N, T}
    if T <: Tuple
        aggregate
    else
        T(aggregate...)
    end
end

function interpolate(bary, face, vn::Val{N}, aggregate = ()) where N
    @inbounds begin
        res = (
            bary[1] * getfield(face[1], N) .+
            bary[2] * getfield(face[2], N) .+
            bary[3] * getfield(face[3], N)
        )
    end
    return interpolate(bary, face, Next(vn), (res, aggregate...))
end

broadcastmin(a, b) = min.(a, b)
broadcastmax(a, b) = max.(a, b)

function clip2pixel_space(position, resolution)
    clipspace = position ./ position[4]
    p = clipspace[Vec(1, 2)]
    (((p .+ 1f0) / 2f0) .* (resolution .- 1f0)) .+ 1f0, clipspace[3]
end

function (r::JLRasterizer{Vert, Args, FragN})(
        canvas, vertex_array::AbstractArray{Vert}, uniforms::Args
    ) where {Vert, Args, FragN}
    framebuffers = canvas.color; depthbuffer = canvas.depth
    resolution = Vec2f(size(framebuffers[1]))
    # hoisting out functions... Seems to help inference a bit. Or not?
    vshader = r.vertexshader
    gshader = r.geometryshader
    fshader = r.fragmentshader
    FragNVal = Val(FragN)
    fragments_drawn = 0
    for face in vertex_array
        vertex_stage = map(reverse(face)) do f
            vshader(f, uniforms...)
        end

        geom_stage = if isnothing(gshader)
            (vertex_stage,)
        else
            reset!(r.geometry_view)
            gshader(r.emit, vertex_stage, uniforms...)
            r.geometry_view.view
        end

        for geom_face in geom_stage
            fdepth = map(geom_face) do vert
                fv = first(vert)
                clip2pixel_space(fv, resolution)
            end
            f = map(first, fdepth)
            depths = map(last, fdepth)
            vertex_out = map(last, geom_face)
            # Bounding rectangle
            mini = max.(reduce(broadcastmin, f), 1f0)
            maxi = min.(reduce(broadcastmax, f), resolution)
            area = edge_function(f[1], f[2], f[3])
            for y = mini[2]:maxi[2]
                for x = mini[1]:maxi[1]
                    p = Vec(x, y)
                    w = Vec(
                        edge_function(f[2], f[3], p),
                        edge_function(f[3], f[1], p),
                        edge_function(f[1], f[2], p)
                    )
                    yi, xi = round(Int, y), round(Int, x)
                    if all(w-> w >= 0f0, w) && checkbounds(Bool, framebuffers[1], yi, xi)

                        bary = w / area
                        depth = bary[1] * depths[1] + bary[2] * depths[2] + bary[3] * depths[3]

                        if depth <= depthbuffer[yi, xi]
                            depthbuffer[yi, xi] = depth
                            fragment_in = interpolate(bary, vertex_out, FragNVal)
                            fragment_out = fshader(fragment_in, uniforms...)
                            for i = eachindex(fragment_out)
                                src_color = framebuffers[i][yi, xi]
                                dest_color = fragment_out[i]
                                fragments_drawn += 1
                                framebuffers[i][yi, xi] = standard_transparency(
                                    src_color,
                                    RGBA{Float32}(dest_color[1], dest_color[2], dest_color[3], dest_color[4])
                                )
                            end
                        end
                    end
                end
            end
        end
    end
    println("fragments drawn: ", fragments_drawn)
    return
end

"""
smoothstep performs smooth Hermite interpolation between 0 and 1 when edge0 < x < edge1. This is useful in cases where a threshold function with a smooth transition is desired. smoothstep is equivalent to:
```
    t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
    return t * t * (3.0 - 2.0 * t);
```
Results are undefined if edge0 ≥ edge1.
"""
function smoothstep(edge0, edge1, x::T) where T
    t = clamp.((x .- edge0) ./ (edge1 .- edge0), T(0), T(1))
    return t * t * (T(3) - T(2) * t)
end

function aastep(threshold1::T, value) where T
    afwidth = norm(Vec2f(dFdx(value), dFdy(value))) * T(1.05);
    smoothstep(threshold1 - afwidth, threshold1 + afwidth, value)
end

function aastep(threshold1::T, threshold2::T, value::T) where T
    afwidth = norm(Vec2f(dFdx(value), dFdy(value))) * T(1.05);
    return (
        smoothstep(threshold1 - afwidth, threshold1 + afwidth, value) -
        smoothstep(threshold2 - afwidth, threshold2 + afwidth, value)
    )
end

mix(x, y, a::T) where {T} = x .* (T(1) .- a) .+ y .* a
fract(x) = x - floor(x)
fabs(x::AbstractFloat) = abs(x)

"""
Gradient in x direction
This is sadly a bit hard to implement for a pure CPU versions, since it's pretty much backed into the GPU hardware.
How it seems to work is, that it takes the values from neighboring registers, which work in parallel on the pixels
of the triangle, so they actually do hold the neighboring values needed to calculate the gradient.
"""
dFdx(value::T) where {T} = T(0.001) # just default to a small gradient if it's called on the CPU
dFdy(value::T) where {T} = T(0.001) # just default to a small gradient if it's called on the CPU

mutable struct Uniforms{F}
    projection::Mat4f
    strokecolor::Vec4f
    glowcolor::Vec4f
    distance_func::F
end

struct VertexCS{N, T}
    position::Vec{N, T}
    color::Vec4f
    scale::Vec2f
end

struct Vertex2Geom
    uvrect::Vec4f
    color::Vec4f
    rect::Vec4f
end

function vert_particles(vertex, uniforms)
    p = vertex.position
    scale = vertex.scale
    return Vertex2Geom(
        Vec4f(0,0,1,1),
        vertex.color,
        Vec4f(p[1], p[2], scale[1], scale[2])
    )
end

"""
Emits a vertex with
"""
function emit_vertex(emit!, vertex, uv, arg, pos, uniforms)
    datapoint = uniforms.projection * Vec4f(pos[1], pos[2], 0, 1)
    final_position = uniforms.projection * Vec4f(vertex[1], vertex[2], 0, 0)
    emit!(datapoint .+ final_position, (uv, arg.color))
    return
end

function geom_particles(emit!, vertex_out, uniforms, image)
    geom_particles(emit!, vertex_out, uniforms)
    return
end

function geom_particles(emit!, vertex_out, uniforms)
    # get arguments from first face
    # (there is only one in there anywas, since primitive type is point)
    # (position, vertex_out)
    arg = vertex_out[1]
    # emit quad as triangle strip
    # v3. ____ . v4
    #    |\   |
    #    | \  |
    #    |  \ |
    #    |___\|
    # v1*      * v2
    pos_scale = arg.rect
    pos = pos_scale[Vec(1, 2)]
    scale = pos_scale[Vec(3, 4)]
    quad = Vec4f(0f0, 0f0, scale[1], scale[2])
    uv = arg.uvrect
    emit_vertex(emit!, quad[Vec(1, 2)], uv[Vec(1, 4)], arg, pos, uniforms)
    emit_vertex(emit!, quad[Vec(1, 4)], uv[Vec(1, 2)], arg, pos, uniforms)
    emit_vertex(emit!, quad[Vec(3, 2)], uv[Vec(3, 4)], arg, pos, uniforms)
    emit_vertex(emit!, quad[Vec(3, 4)], uv[Vec(3, 2)], arg, pos, uniforms)
    return
end

function sdf2color(dist, bg_color, color)
    inside = aastep(0f0, dist)
    mix(bg_color, color, inside)
end

function frag_particles(geom_out, uniforms, image)
    uv = geom_out[1]; color = geom_out[2]
    dist = -image[uv][1]
    bg_color = Vec4f(0f0, 0f0, 0f0, 0f0)
    (sdf2color(dist, bg_color, color), )
end

function frag_particles(geom_out, uniforms)
    uv = geom_out[1]; color = geom_out[2]
    dist = uniforms.distance_func(uv)
    bg_color = Vec4f(0f0, 0f0, 0f0, 0f0)
    # col = Vec4f(norm(uv .- 0.5), 0, 0, 1)
    (sdf2color(dist, bg_color, color), )
end

resolution = (1024, 1024)

proj = Makie.orthographicprojection(Rect2(0, 0, resolution...), -10_000f0, 10_000f0)

circle(uv::Vec{2, T}) where {T} = -T(norm(uv .- 0.5) - 0.5)

uniforms = Uniforms(
    proj,
    Vec4f(1, 0, 0, 1),
    Vec4f(1, 0, 1, 1),
    circle
)

N = 10
middle = Vec2f(resolution) / 2f0
radius = (min(resolution...) / 2f0) - 50
vertices = [(VertexCS(
    Vec2f((sin(2pi * (i / N)) , cos(2pi * (i / N))) .* radius) .+ middle,
    Vec4f(1, i/N, 0, 1),
    Vec2f(40, 40)
),) for i = 1:N]

raster, rest = rasterizer(
    vertices,
    (uniforms,),
    vert_particles,
    frag_particles;
    geometryshader = geom_particles,
    max_primitives = 4,
    primitive_in = :points,
    primitive_out = :triangle_strip,
)

struct Canvas{N}
    color::NTuple{N, Matrix{RGBA{Float32}}}
    depth::Matrix{Float32}
end

function Canvas(xdim::Integer, ydim::Integer)
    color = fill(RGBA{Float32}(0,0,0,0), xdim, ydim)
    depth = ones(Float32, xdim, ydim)
    return Canvas((color,), depth)
end

c = Canvas(resolution...)
raster(c, vertices, (uniforms,))
save("test.png", c.color[1])


begin
    local b = startboard()
    @test pieceon(b, SQ_E1) == PIECE_WK
    @test pieceon(b, FILE_B, RANK_8) == PIECE_BN
    @test pieceon(b, SQ_B5) == EMPTY
end

begin
    local b = startboard()
    local b2 = domove(b, movefromstring("e2e4"))
    @test sidetomove(b) == WHITE
    @test sidetomove(b2) == BLACK
end

begin
    local b = startboard()
    @test epsquare(b) == SQ_NONE
    local b2 = fromfen("rnbqkbnr/pppp1ppp/4p3/4P3/8/8/PPPP1PPP/RNBQKBNR b - -")
    @test epsquare(b2) == SQ_NONE
    local b3 = domove(b2, movefromstring("d7d5"))
    @test epsquare(b3) == SQ_D6
end

begin
    local b = startboard()
    @test pieces(b, WHITE) == SS_RANK_1 ∪ SS_RANK_2
    @test pieces(b, ROOK) == SquareSet(SQ_A1, SQ_H1, SQ_A8, SQ_H8)
    @test pieces(b, BLACK, PAWN) == SS_RANK_7
    @test pieces(b, PIECE_WB) == SquareSet(SQ_C1, SQ_F1)
    @test pawns(b) == SS_RANK_2 ∪ SS_RANK_7
    @test pawns(b, WHITE) == SS_RANK_2
    @test pawns(b, BLACK) == SS_RANK_7
    @test knights(b) == SquareSet(SQ_B1, SQ_G1, SQ_B8, SQ_G8)
    @test knights(b, WHITE) == SquareSet(SQ_B1, SQ_G1)
    @test knights(b, BLACK) == SquareSet(SQ_B8, SQ_G8)
    @test bishops(b) == SquareSet(SQ_C1, SQ_F1, SQ_C8, SQ_F8)
    @test bishops(b, WHITE) == SquareSet(SQ_C1, SQ_F1)
    @test bishops(b, BLACK) == SquareSet(SQ_C8, SQ_F8)
    @test rooks(b) == SquareSet(SQ_A1, SQ_H1, SQ_A8, SQ_H8)
    @test rooks(b, WHITE) == SquareSet(SQ_A1, SQ_H1)
    @test rooks(b, BLACK) == SquareSet(SQ_A8, SQ_H8)
    @test queens(b) == SquareSet(SQ_D1, SQ_D8)
    @test queens(b, WHITE) == SquareSet(SQ_D1)
    @test queens(b, BLACK) == SquareSet(SQ_D8)
    @test kings(b) == SquareSet(SQ_E1, SQ_E8)
    @test kings(b, WHITE) == SquareSet(SQ_E1)
    @test kings(b, BLACK) == SquareSet(SQ_E8)
    @test bishoplike(b) == SquareSet(SQ_C1, SQ_D1, SQ_F1, SQ_C8, SQ_D8, SQ_F8)
    @test bishoplike(b, WHITE) == SquareSet(SQ_C1, SQ_D1, SQ_F1)
    @test bishoplike(b, BLACK) == SquareSet(SQ_C8, SQ_D8, SQ_F8)
    @test rooklike(b) == SquareSet(SQ_A1, SQ_D1, SQ_H1, SQ_A8, SQ_D8, SQ_H8)
    @test rooklike(b, WHITE) == SquareSet(SQ_A1, SQ_D1, SQ_H1)
    @test rooklike(b, BLACK) == SquareSet(SQ_A8, SQ_D8, SQ_H8)
end

begin
    local b = fromfen("5k2/8/4q3/8/2B5/8/4P3/3K4 w - -")
    @test bishopattacks(b, SQ_C4) ==
          SquareSet(SQ_A2, SQ_B3, SQ_E2, SQ_D3, SQ_B5, SQ_A6, SQ_D5, SQ_E6)
    local b = fromfen("2r2k2/8/8/8/2R3P1/8/4P3/3K4 w - -")
    @test rookattacks(b, SQ_C4) == SquareSet(
        SQ_A4,
        SQ_B4,
        SQ_D4,
        SQ_E4,
        SQ_F4,
        SQ_G4,
        SQ_C1,
        SQ_C2,
        SQ_C3,
        SQ_C5,
        SQ_C6,
        SQ_C7,
        SQ_C8,
    )
    local b = fromfen("2r2k2/8/8/8/2Q3P1/8/4P3/3K4 w - -")
    @test queenattacks(b, SQ_C4) == SquareSet(
        SQ_C1,
        SQ_C2,
        SQ_C3,
        SQ_C5,
        SQ_C6,
        SQ_C7,
        SQ_C8,
        SQ_A4,
        SQ_B4,
        SQ_D4,
        SQ_E4,
        SQ_F4,
        SQ_G4,
        SQ_A2,
        SQ_B3,
        SQ_D5,
        SQ_E6,
        SQ_F7,
        SQ_G8,
        SQ_E2,
        SQ_D3,
        SQ_B5,
        SQ_A6,
    )
end

begin
    local b = startboard()
    @test isattacked(b, SQ_F3, WHITE)
    @test !isattacked(b, SQ_F3, BLACK)
end

begin
    local b = fromfen("r1bqkbnr/pppp1ppp/2n5/4p3/3PP3/5N2/PPP2PPP/RNBQKB1R b KQkq - 0 3")
    @test attacksto(b, SQ_D4) == SquareSet(SQ_D1, SQ_F3, SQ_E5, SQ_C6)
end

begin
    local b = fromfen("2r4b/1kp5/8/2P1Q3/1P6/2K1P2r/8/8 w - -")
    @test pinned(b) == SquareSet(SQ_E3, SQ_E5)
end

begin
    local b = startboard()
    @test perft(b, 0) == 1
    @test perft(b, 1) == 20
    @test perft(b, 2) == 400
    @test perft(b, 3) == 8902
    @test perft(b, 4) == 197281
    @test perft(b, 5) == 4865609
    # @test perft(b, 6) == 119060324
    # @test perft(b, 7) == 3195901860

    b = fromfen("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -")
    @test perft(b, 0) == 1
    @test perft(b, 1) == 48
    @test perft(b, 2) == 2039
    @test perft(b, 3) == 97862
    @test perft(b, 4) == 4085603
    # @test perft(b, 5) == 193690690
    # @test perft(b, 6) == 8031647685

    b = fromfen("8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - -")
    @test perft(b, 0) == 1
    @test perft(b, 1) == 14
    @test perft(b, 2) == 191
    @test perft(b, 3) == 2812
    @test perft(b, 4) == 43238
    @test perft(b, 5) == 674624
    # @test perft(b, 6) == 11030083
    # @test perft(b, 7) == 178633661

    b = fromfen("bqnb1rkr/pp3ppp/3ppn2/2p5/5P2/P2P4/NPP1P1PP/BQ1BNRKR w HFhf -")
    @test perft(b, 0) == 1
    @test perft(b, 1) == 21
    @test perft(b, 2) == 528
    @test perft(b, 3) == 12189
    @test perft(b, 4) == 326672
    @test perft(b, 5) == 8146062

    b = fromfen("b1qr1krb/pp1ppppp/n2n4/8/2p5/2P3P1/PP1PPP1P/BNQRNKRB w GDgd -")
    @test perft(b, 0) == 1
    @test perft(b, 1) == 28
    @test perft(b, 2) == 707
    @test perft(b, 3) == 19721
    @test perft(b, 4) == 549506
    @test perft(b, 5) == 15583376

    b = fromfen("nrnbqkbr/2pp2pp/4pp2/pp6/8/1P3P2/P1PPPBPP/NRNBQ1KR w hb -")
    @test perft(b, 0) == 1
    @test perft(b, 1) == 25
    @test perft(b, 2) == 656
    @test perft(b, 3) == 16951
    @test perft(b, 4) == 466493
    @test perft(b, 5) == 12525939

    b = fromfen("qnbbrknr/1p1ppppp/8/p1p5/5P2/PP1P4/2P1P1PP/QNBBRKNR w HEhe -")
    @test perft(b, 0) == 1
    @test perft(b, 1) == 27
    @test perft(b, 2) == 573
    @test perft(b, 3) == 16331
    @test perft(b, 4) == 391656
    @test perft(b, 5) == 11562434

    b = fromfen("nrq1kbnr/p1pbpppp/3p4/1p6/6P1/1N3N2/PPPPPP1P/1RBQKB1R w HBhb -")
    @test perft(b, 0) == 1
    @test perft(b, 1) == 24
    @test perft(b, 2) == 648
    @test perft(b, 3) == 16640
    @test perft(b, 4) == 471192
    @test perft(b, 5) == 12871967

    b = fromfen("1qrkrnbb/1p1p1ppp/pnp1p3/8/3PP3/P6P/1PP2PP1/NQRKRNBB w ECec -")
    @test perft(b, 0) == 1
    @test perft(b, 1) == 24
    @test perft(b, 2) == 688
    @test perft(b, 3) == 17342
    @test perft(b, 4) == 511444
    @test perft(b, 5) == 13322502

    b = fromfen("1brnqknr/2p1pppp/p2p4/1P6/6P1/4Nb2/PP1PPP1P/BBR1QKNR w HChc -")
    @test perft(b, 0) == 1
    @test perft(b, 1) == 34
    @test perft(b, 2) == 1019
    @test perft(b, 3) == 32982
    @test perft(b, 4) == 1003103
    @test perft(b, 5) == 33322477

    b = fromfen("brn1kbrn/pp2p1pp/3p4/q1p2p2/2P4P/6P1/PP1PPP2/BRNQKBRN w GBgb -")
    @test perft(b, 0) == 1
    @test perft(b, 1) == 18
    @test perft(b, 2) == 477
    @test perft(b, 3) == 10205
    @test perft(b, 4) == 273925
    @test perft(b, 5) == 6720181

    b = fromfen("b1rknnrq/bpppp1p1/p6p/5p1P/6P1/4N3/PPPPPP2/BBRKN1RQ w GCgc -")
    @test perft(b, 0) == 1
    @test perft(b, 1) == 33
    @test perft(b, 2) == 851
    @test perft(b, 3) == 28888
    @test perft(b, 4) == 763967
    @test perft(b, 5) == 26686205

    b = fromfen("brkqnrnb/1p1pp1p1/p4p2/2p4p/8/P2PP3/1PP1QPPP/BRK1NRNB w FBfb -")
    @test perft(b, 0) == 1
    @test perft(b, 1) == 24
    @test perft(b, 2) == 479
    @test perft(b, 3) == 12584
    @test perft(b, 4) == 280081
    @test perft(b, 5) == 7830230

    b = fromfen("brkrqb1n/1pppp1pp/p7/3n1p2/P5P1/3PP3/1PP2P1P/BRKRQBNN w DBdb -")
    @test perft(b, 0) == 1
    @test perft(b, 1) == 27
    @test perft(b, 2) == 669
    @test perft(b, 3) == 18682
    @test perft(b, 4) == 484259
    @test perft(b, 5) == 13956472
end

begin
    fens = [
        "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -",
        "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w Kq -",
        "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w Qk -",
        "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - -",
        "rnbqkbnr/pppp1ppp/4p3/4P3/8/8/PPPP1PPP/RNBQKBNR w KQkq d6",
    ]
    for f in fens
        @test fen(fromfen(f)) == f
    end
end

function computekey(b::Board)::UInt64
    result = UInt64(0)
    for s in occupiedsquares(b)
        p = pieceon(b, s)
        result ⊻= Chess.zobrist(p, s)
    end
    result ⊻= Chess.zobcastle(b.castlerights)
    if epsquare(b) != SQ_NONE
        result ⊻= Chess.zobep(epsquare(b))
    end
    if sidetomove(b) == BLACK
        result ⊻= Chess.zobsidetomove()
    end
    result
end

function keyisright(b::Board)::Bool
    b.key == computekey(b)
end

begin
    b = fromfen("r3k2r/1P6/8/3Pp3/5p2/8/4P3/R3K2R w KQkq e6")
    @test keyisright(b)

    ms = ["bxa8=R+", "b8=N", "dxe6", "e4", "O-O", "Rxh8+"]
    for m in ms
        @test keyisright(domove(b, m))
    end

    for m in ms
        u = domove!(b, m)
        @test keyisright(b)
        undomove!(b, u)
        @test keyisright(b)
    end
end

begin
    @test isstalemate(fromfen("b5bk/3r4/2PNP3/1rNKNr2/2PNP3/3r4/b5b1/8 w - -"))
    @test !isstalemate(fromfen("b5bk/3r4/2PNP3/1rNKNr2/2PNP3/3r4/b7/8 w - -"))
    @test ischeckmate(fromfen("b5bk/3r4/2P1P3/1rNKNr2/2PNP3/3r4/b5b1/8 w - -"))
    @test !ischeckmate(fromfen("6bk/3r4/2P1P3/1rNKNr2/2PNP3/3r4/b5b1/8 w - -"))
    @test ismaterialdraw(fromfen("8/8/3k4/8/3K4/8/8/8 w - -"))
    @test ismaterialdraw(fromfen("8/8/3k4/8/3K4/8/8/7N w - -"))
    @test ismaterialdraw(fromfen("8/8/3k4/8/3K4/8/8/7b w - -"))
    @test !ismaterialdraw(fromfen("8/8/3k4/8/3K4/8/8/7R w - -"))
    @test !ismaterialdraw(fromfen("8/8/3k4/8/3K4/8/8/7q w - -"))
    @test !ismaterialdraw(fromfen("8/8/3k4/8/3K4/8/7b/7N w - -"))
    @test !ismaterialdraw(fromfen("8/8/3k4/8/3K4/8/8/BB6 w - -"))
    @test !ismaterialdraw(fromfen("8/8/3k4/8/3K4/8/3P4/8 w - -"))
end

begin
    b = fromfen("8/4k3/3p4/4p3/8/2BK4/8/8 w - - 0 1")
    @test see(b, Move(SQ_C3, SQ_E5)) == -2
    b = fromfen("8/4k3/3p4/4p3/8/2BK4/8/Q7 w - - 0 1")
    @test see(b, Move(SQ_C3, SQ_E5)) == -1
    b = fromfen("8/4k3/8/4p3/8/2BK4/8/q7 w - - 0 1")
    @test see(b, Move(SQ_C3, SQ_E5)) == -2
    b = fromfen("8/4k3/8/4p3/8/2BK4/8/8 w - - 0 1")
    @test see(b, Move(SQ_C3, SQ_E5)) == 1
    b = fromfen("7q/4k1b1/3p4/4n3/8/2BK1N2/4R3/4R3 w - - 0 1")
    @test see(b, Move(SQ_F3, SQ_E5)) == 1
    b = fromfen("7q/4k1b1/3p4/4n3/8/2BK1N2/4R3/4R3 w - - 0 1")
    @test see(b, Move(SQ_E2, SQ_E5)) == -1
    b = fromfen("7q/4k1b1/3p4/4n3/8/2BK1N2/4R3/8 w - - 0 1")
    @test see(b, Move(SQ_E2, SQ_E5)) == -2
    b = fromfen("7q/4k1b1/3p4/4n3/8/2BK1N2/4R3/8 w - - 0 1")
    @test see(b, Move(SQ_C3, SQ_E5)) == 0
    b = fromfen("8/2b5/3k4/4p3/3K1P2/8/8/8 w - -")
    @test see(b, Move(SQ_F4, SQ_E5)) == 1
    b = fromfen("8/8/3k1p2/4p3/3K1P2/2B5/8/8 w - -")
    @test see(b, Move(SQ_F4, SQ_E5)) == 0
end

begin
    b = fromfen("r1bqk2r/p4pb1/2pp1np1/6P1/2PPp3/2N5/PP3PB1/R1BQK2R b KQkq -")
    domove!(b, "Rxh1+")
    @test fen(b) == "r1bqk3/p4pb1/2pp1np1/6P1/2PPp3/2N5/PP3PB1/R1BQK2r w Qq -"
end

begin
    b = startboard()
    @test fen(decompress(compress(b))) == fen(b)
    b = fromfen("4k3/8/8/pP6/8/8/8/4K3 w - a6")
    @test fen(decompress(compress(b))) == fen(b)
    b = fromfen("r3k2r/8/8/8/Pp6/8/8/4K3 b kq a3")
    @test fen(decompress(compress(b))) == fen(b)
end


using CartesianGeneticProgramming
using Cambrian
using ArcadeLearningEnvironment
using ArgParse
import Random

export AtariEnv, fitness, new_gen!, atari_envs

include("game.jl")

```
Playing Atari games on RAM values

This uses a single Game seed, meaning an unfair deterministic Atari
environment. To evolve using a different game seed per generation, uncomment code in new_gen!
```

"Atari BERL environment"
mutable struct AtariEnv <: BERLenv
    name::String
    memory::Array # Array of storable data
    env_name
    seed
end

environments["atari_ram"]=AtariEnv
atari_envs=["centipede"]

function AtariEnv(cfg::Dict)
    env_name = get!(cfg, "atari_game", "centipede")
    cfg["env"]="Atari - $env_name"
    game = Game(env_name, 0)
    cfg["n_in"] = length(get_ram(game))
    cfg["n_out"] = length(game.actions)
    close!(game)

    AtariEnv(cfg["env"],  [], env_name, 0)
end

"Returns the fitness of one Cambrian Individual in this environment"
function fitness(indiv::Cambrian.Individual, env::AtariEnv; seed=0, max_frames=18000)
    game = Game(env.env_name, env.seed)
    reward = 0.0
    frames = 0
    while ~game_over(game.ale)
        output = process(indiv, get_ram(game))
        action = game.actions[argmax(output)]
        reward += act(game.ale, action)
        frames += 1
        if frames > max_frames
            break
        end
    end
    close!(game)
    [reward]
end

"Sets up the env for the next generation"
function new_gen!(env::AtariEnv, cfg::Dict=nothing)
    # env.seed += 1
end


println("Dn) Test of the ImpactExcitation module with ASF from an internally generated initial- and final-state multiplet.")

@warn("\n\n !!! This example does not work properly at present !!! \n\n")
#
setDefaults("print summary: open", "zzz-ImpactExcitation.sum")
wa = Atomic.Computation(Atomic.Computation(), name="xx", grid=JAC.Radial.Grid(true), nuclearModel=Nuclear.Model(36.),
                        initialConfigs=[Configuration("1s^2 2s^2 2p^6")],
                        finalConfigs  =[Configuration("1s^2 2s^2 2p^5"), Configuration("1s^2 2s 2p^6") ],
                        process = Eimex(),
                        processSettings=ImpactExcitation.Settings([100., 200.], false, true, LineSelection(), 2, 0.) )

wb = perform(wa)


using Revise
using AutonomousMerging
using AutomotiveDrivingModels
using Random
using POMDPs
using POMDPSimulators
using DeepQLearning
using Flux
using BSON
using Reel
using RLInterface
using AutoViz
AutoViz.set_color_theme(OFFICETHEME)
include("scripts/make_gif.jl")

rng = MersenneTwister(1)

mdp = GenerativeMergingMDP(random_n_cars=true, min_cars=16,max_cars=16, traffic_speed = :mixed, driver_type=:binary, initial_ego_velocity=5.0,
                           observe_cooperation=true);
for i=2:mdp.max_cars+1
    mdp.driver_models[i] = CooperativeIDM();
end
# pomdp = FullyObservablePOMDP(mdp)
# up = MergingUpdater(mdp)
# bmdp = GenerativeBeliefMDP{typeof(pomdp), MergingUpdater, MergingBelief,Int64}(pomdp, up)

s0 = initialstate(mdp, rng)

svec = convert_s(Vector{Float64}, s0, mdp)

input_dims = length(svec)

model = Chain(Dense(input_dims, 64, relu), Dense(64, 32, relu), Dense(32, n_actions(mdp)))
# do not run if you just want to load a policy
# solver = DeepQLearningSolver(qnetwork = model,
#                       max_steps = 1_000_000,
#                       eps_fraction = 0.5,
#                       eps_end = 0.01,
#                       eval_freq = 10_000,
#                       save_freq = 10_000,
#                       target_update_freq = 5000,
#                       batch_size = 32,
#                       learning_rate = 1e-4,
#                       train_start = 10000,
#                       log_freq = 1000,
#                       num_ep_eval = 1000,
#                       double_q = true,
#                       dueling = false,
#                       prioritized_replay = true,
#                       verbose = true,
#                       rng = rng)

# policy = solve(solver, mdp)

# BSON.@save "policy_true.bson" policy

# BSON.@load joinpath(solver.logdir,"policy_true.bson") policy
BSON.@load "log15/qnetwork.bson" qnetwork
Flux.loadparams!(model, qnetwork)
policy = NNPolicy(mdp, model, collect(actions(mdp)), 1);

env = MDPEnvironment(mdp)
DeepQLearning.evaluation(solver.evaluation_policy,
                policy, env,
                solver.num_ep_eval,
                solver.max_episode_length,
                solver.verbose)

# policy = MaskedPolicy(policy)

for i=1:10
    s0 = initialstate(mdp, rng);
    hr = HistoryRecorder(rng = rng, max_steps=100);
    hist = simulate(hr, mdp, policy, s0);
    @show undiscounted_reward(hist)
    if undiscounted_reward(hist) > 0.0
        make_gif(hist, mdp, nothing, "out$i.gif")
    end
end

include("visualizer.jl");

i = 36
s = state_hist(hist)[i]
c = AutoViz.render(s.scene, mdp.env.roadway,  cam = StaticCamera(VecE2(-25.0, -10.0), 10.0),
                [MergingNeighborsOverlay(target_id=EGO_ID, env=mdp.env)],
                car_colors=get_car_type_colors(s0.scene, mdp.driver_models))

write_to_png(c, "snap-bis$i.png")

i = 36
collect(action_hist(hist))[i], state_hist(hist)[i].ego_info.acc, get_by_id(state_hist(hist)[i].scene, 1).state.v

make_gif(hist, mdp)

## run many simulations

function quick_evaluation(mdp::MDP, policy::Policy, rng::AbstractRNG, n_eval=1000)
    avg_r, avg_dr, c_rate, avg_steps, t_out = 0.0, 0.0, 0.0, 0.0, 0.0
    @showprogress for i=1:n_eval
        s0 = initialstate(mdp, rng)
        hr = HistoryRecorder(rng = rng, max_steps=100)
        hist = simulate(hr, mdp, policy, s0)
        avg_r += undiscounted_reward(hist)
        avg_dr += discounted_reward(hist)
        c_rate += undiscounted_reward(hist) <= -1
        t_out += n_steps(hist) >= hr.max_steps ? 1.0 : 0.0
        avg_steps += n_steps(hist)
    end
    avg_r /= n_eval
    avg_dr /= n_eval
    c_rate /= n_eval
    avg_steps /= n_eval
    t_out /= n_eval
    return avg_r, avg_dr, c_rate, avg_steps, t_out
end

avg_r, avg_dr, c_rate, avg_steps, t_out = quick_evaluation(bmdp, policy, rng, 100)

println("Collisions ", c_rate*100)
println("Avg steps ", avg_steps)
println("Time outs ", t_out*100)
println("avg disc reward ", avg_dr)
println("avg reward ", avg_r)

simlist = [Sim(mdp, policy, max_steps=200, rng=MersenneTwister(i)) for i=1:1000];

res = run_parallel(simlist) do sim, hist
    return [:steps=>n_steps(hist), :dreward=>discounted_reward(hist), :reward=>undiscounted_reward(hist)]
end

n_collisions = sum(res[:reward] .< 0.0)
avg_steps = mean(res[:steps])
avg_dreward = mean(res[:dreward])
avg_reward = mean(res[:reward])
collision_ind = findfirst(res[:reward] .< 0.0)
rng = MersenneTwister(collision_ind)

s0 = initialstate(mdp, rng)
hr = HistoryRecorder(rng = rng, max_steps=100)
hist = simulate(hr, mdp, policy, s0)
n_steps(hist)
undiscounted_reward(hist)

include("visualizer.jl");

frames = Frames(MIME("image/png"), fps=4);
for step in 1:n_steps(hist)
    s = state_hist(hist)[step+1]
    a = collect(action_hist(hist))[step]
    f = AutoViz.render(s.scene, mdp.env.roadway,
          SceneOverlay[
                       IDOverlay(),
                       MergingNeighborsOverlay(target_id=EGO_ID, env=mdp.env),
                       DistToMergeOverlay(target_id=EGO_ID, env=mdp.env),
                       MaskingOverlay(mdp=mdp)
                       #    NeighborsOverlay(EGO_ID),
                    #    CarFollowingStatsOverlay(EGO_ID),
                        ],
          cam=CarFollowCamera(EGO_ID, 10.0),
          car_colors = Dict{Int64, Colorant}(1 => COLOR_CAR_EGO))
    push!(frames, f)
end

write("out.gif", frames)

## Env version

obs = reset!(env)
act = action(policy, op)
vals = actionvalues(policy, op)
op, rew, done, info = step!(env, act)

s = env.state

action(policy, s)
actionvalues(policy, s)

using ProgressMeter
avg_r = 0.0
@showprogress for i=1:1000
    global avg_r
    s0 = initialstate(mdp, rng)
    hr = HistoryRecorder(rng = rng, max_steps=100)
    hist = simulate(hr, mdp, policy, s0)
    avg_r += undiscounted_reward(hist)
end

avg_r/1000


@testset "Test Reserves from Thermal Dispatch" begin
    template = get_thermal_dispatch_template_network(CopperPlatePowerModel)
    set_service_model!(template, ServiceModel(VariableReserve{ReserveUp}, RangeReserve))
    set_service_model!(template, ServiceModel(VariableReserve{ReserveDown}, RangeReserve))
    set_service_model!(
        template,
        ServiceModel(ReserveDemandCurve{ReserveUp}, StepwiseCostReserve),
    )

    c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true)
    for p in [true, false]
        op_problem = OperationsProblem(template, c_sys5_uc; use_parameters = p)
        @test build!(op_problem; output_dir = mktempdir(cleanup = true)) ==
              PSI.BuildStatus.BUILT
        moi_tests(op_problem, p, 648, 0, 120, 216, 72, false)
        symbols = [
            :Reserve1__VariableReserve_ReserveUp,
            :Reserve11__VariableReserve_ReserveUp,
            :Reserve2__VariableReserve_ReserveDown,
            :ORDC1__ReserveDemandCurve_ReserveUp,
        ]
        for sym in symbols
            for v in op_problem.internal.optimization_container.variables[sym]
                @test JuMP.has_lower_bound(v)
                @test JuMP.lower_bound(v) == 0.0
            end
        end
    end
end

@testset "Test Reserves from Thermal Standard UC" begin
    template = get_thermal_standard_uc_template()
    set_service_model!(template, ServiceModel(VariableReserve{ReserveUp}, RangeReserve))
    set_service_model!(template, ServiceModel(VariableReserve{ReserveDown}, RangeReserve))
    set_service_model!(
        template,
        ServiceModel(ReserveDemandCurve{ReserveUp}, StepwiseCostReserve),
    )
    c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true)

    for p in [true, false]
        op_problem = OperationsProblem(template, c_sys5_uc; use_parameters = p)
        @test build!(op_problem; output_dir = mktempdir(cleanup = true)) ==
              PSI.BuildStatus.BUILT
        moi_tests(op_problem, p, 1008, 0, 480, 216, 192, true)
    end
end

@testset "Test Upwards Reserves from Renewable Dispatch" begin
    template = OperationsProblemTemplate(CopperPlatePowerModel)
    set_device_model!(template, PowerLoad, StaticPowerLoad)
    set_device_model!(template, RenewableDispatch, RenewableFullDispatch)
    set_service_model!(template, ServiceModel(VariableReserve{ReserveUp}, RangeReserve))
    set_service_model!(
        template,
        ServiceModel(ReserveDemandCurve{ReserveUp}, StepwiseCostReserve),
    )

    c_sys5_re = PSB.build_system(PSITestSystems, "c_sys5_re"; add_reserves = true)
    for p in [true, false]
        op_problem = OperationsProblem(template, c_sys5_re; use_parameters = p)
        @test build!(op_problem; output_dir = mktempdir(cleanup = true)) ==
              PSI.BuildStatus.BUILT
        moi_tests(op_problem, p, 360, 0, 72, 48, 72, false)
    end
end

@testset "Test Reserves from Storage" begin
    template = get_thermal_dispatch_template_network(CopperPlatePowerModel)
    set_device_model!(template, GenericBattery, BookKeeping)
    set_device_model!(template, RenewableDispatch, FixedOutput)
    set_service_model!(template, ServiceModel(VariableReserve{ReserveUp}, RangeReserve))
    set_service_model!(template, ServiceModel(VariableReserve{ReserveDown}, RangeReserve))
    set_service_model!(
        template,
        ServiceModel(ReserveDemandCurve{ReserveUp}, StepwiseCostReserve),
    )

    c_sys5_bat = PSB.build_system(PSITestSystems, "c_sys5_bat"; add_reserves = true)
    for p in [true, false]
        op_problem = OperationsProblem(template, c_sys5_bat; use_parameters = p)
        @test build!(op_problem; output_dir = mktempdir(cleanup = true)) ==
              PSI.BuildStatus.BUILT
        moi_tests(op_problem, p, 408, 0, 192, 264, 96, false)
    end
end

@testset "Test Reserves from Hydro" begin
    template = OperationsProblemTemplate(CopperPlatePowerModel)
    set_device_model!(template, PowerLoad, StaticPowerLoad)
    set_device_model!(template, HydroEnergyReservoir, HydroDispatchRunOfRiver)
    set_service_model!(template, ServiceModel(VariableReserve{ReserveUp}, RangeReserve))
    set_service_model!(template, ServiceModel(VariableReserve{ReserveDown}, RangeReserve))
    set_service_model!(
        template,
        ServiceModel(ReserveDemandCurve{ReserveUp}, StepwiseCostReserve),
    )

    c_sys5_hyd = PSB.build_system(PSITestSystems, "c_sys5_hyd"; add_reserves = true)
    for p in [true, false]
        op_problem = OperationsProblem(template, c_sys5_hyd; use_parameters = p)
        @test build!(op_problem; output_dir = mktempdir(cleanup = true)) ==
              PSI.BuildStatus.BUILT
        moi_tests(op_problem, p, 240, 0, 24, 96, 72, false)
    end
end

@testset "Test Reserves from with slack variables" begin
    template = get_thermal_dispatch_template_network()
    set_service_model!(template, ServiceModel(VariableReserve{ReserveUp}, RangeReserve))
    set_service_model!(template, ServiceModel(VariableReserve{ReserveDown}, RangeReserve))

    c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true)
    for p in [true, false]
        op_problem = OperationsProblem(
            template,
            c_sys5_uc;
            use_parameters = p,
            services_slack_variables = true,
            balance_slack_variables = true,
        )
        @test build!(op_problem; output_dir = mktempdir(cleanup = true)) ==
              PSI.BuildStatus.BUILT
        moi_tests(op_problem, p, 504, 0, 120, 192, 24, false)
    end
end

@testset "Test AGC" begin
    c_sys5_reg = PSB.build_system(PSITestSystems, "c_sys5_reg")
    @test_throws ArgumentError template_agc_reserve_deployment(; dummy_arg = 0.0)

    template_agc = template_agc_reserve_deployment()
    agc_problem = OperationsProblem(AGCReserveDeployment, template_agc, c_sys5_reg)
    @test build!(agc_problem; output_dir = mktempdir(cleanup = true)) ==
          PSI.BuildStatus.BUILT
    # These values might change as the AGC model is refined
    moi_tests(agc_problem, false, 720, 0, 480, 0, 384, false)
end

@testset "Test GroupReserve from Thermal Dispatch" begin
    template = get_thermal_dispatch_template_network()
    set_service_model!(template, ServiceModel(VariableReserve{ReserveUp}, RangeReserve))
    set_service_model!(template, ServiceModel(VariableReserve{ReserveDown}, RangeReserve))
    set_service_model!(
        template,
        ServiceModel(ReserveDemandCurve{ReserveUp}, StepwiseCostReserve),
    )
    set_service_model!(
        template,
        ServiceModel(StaticReserveGroup{ReserveDown}, GroupReserve),
    )

    c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true)
    services = get_components(Service, c_sys5_uc)
    contributing_services = Vector{Service}()
    for service in services
        if !(typeof(service) <: PSY.ReserveDemandCurve)
            push!(contributing_services, service)
        end
    end
    groupservice = StaticReserveGroup{ReserveDown}(;
        name = "init",
        available = true,
        requirement = 0.0,
        ext = Dict{String, Any}(),
    )
    add_service!(c_sys5_uc, groupservice, contributing_services)

    for p in [true, false]
        op_problem = OperationsProblem(template, c_sys5_uc; use_parameters = p)
        @test build!(op_problem; output_dir = mktempdir(cleanup = true)) ==
              PSI.BuildStatus.BUILT
        moi_tests(op_problem, p, 648, 0, 120, 240, 72, false)
    end
end

# TODO: Test is broken
#=
@testset "Test GroupReserve Errors" begin
    template = get_thermal_dispatch_template_network()
    set_service_model!(template, ServiceModel(VariableReserve{ReserveUp}, RangeReserve))
    set_service_model!(template, ServiceModel(VariableReserve{ReserveDown}, RangeReserve))
    set_service_model!(
        template,
        ServiceModel(ReserveDemandCurve{ReserveUp}, StepwiseCostReserve),
    )
    set_service_model!(
        template,
        ServiceModel(StaticReserveGroup{ReserveDown}, GroupReserve),
    )

    c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true)
    services = get_components(Service, c_sys5_uc)
    contributing_services = Vector{Service}()
    for service in services
        if !(typeof(service) <: PSY.ReserveDemandCurve)
            push!(contributing_services, service)
        end
    end
    groupservice = StaticReserveGroup{ReserveDown}(;
        name = "init",
        available = true,
        requirement = 0.0,
        ext = Dict{String, Any}(),
    )
    add_service!(c_sys5_uc, groupservice, contributing_services)

    off_service = VariableReserve{ReserveUp}("Reserveoff", true, 0.6, 10)
    push!(groupservice.contributing_services, off_service)

    op_problem = OperationsProblem(template, c_sys5_uc; use_parameters = false)
    @test_logs(
        (:error, r"is not stored"),
        match_mode = :any,
        @test_throws InfrastructureSystems.InvalidValue build!(op_problem; output_dir = mktempdir(cleanup = true))
    )
end
=#

@testset "Test StaticReserve" begin
    template = get_thermal_dispatch_template_network()
    set_service_model!(template, ServiceModel(StaticReserve{ReserveUp}, RangeReserve))

    c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc")
    static_reserve = StaticReserve{ReserveUp}("Reserve3", true, 30, 100)
    add_service!(c_sys5_uc, static_reserve, get_components(ThermalGen, c_sys5_uc))
    op_problem = OperationsProblem(template, c_sys5_uc)
    @test build!(op_problem; output_dir = mktempdir(cleanup = true)) ==
          PSI.BuildStatus.BUILT
    @test typeof(op_problem) <: OperationsProblem
end


function DormandPrince54FieldIntegrator(arg0::Field, arg1::jdouble, arg2::jdouble, arg3::Vector{jdouble}, arg4::Vector{jdouble})
    return DormandPrince54FieldIntegrator((Field, jdouble, jdouble, Vector{jdouble}, Vector{jdouble}), arg0, arg1, arg2, arg3, arg4)
end

function DormandPrince54FieldIntegrator(arg0::Field, arg1::jdouble, arg2::jdouble, arg3::jdouble, arg4::jdouble)
    return DormandPrince54FieldIntegrator((Field, jdouble, jdouble, jdouble, jdouble), arg0, arg1, arg2, arg3, arg4)
end

function get_a(obj::DormandPrince54FieldIntegrator)
    return jcall(obj, "getA", Vector{Vector{RealFieldElement}}, ())
end

function get_b(obj::DormandPrince54FieldIntegrator)
    return jcall(obj, "getB", Vector{RealFieldElement}, ())
end

function get_c(obj::DormandPrince54FieldIntegrator)
    return jcall(obj, "getC", Vector{RealFieldElement}, ())
end

function get_max_growth(obj::EmbeddedRungeKuttaFieldIntegrator)
    return jcall(obj, "getMaxGrowth", RealFieldElement, ())
end

function get_min_reduction(obj::EmbeddedRungeKuttaFieldIntegrator)
    return jcall(obj, "getMinReduction", RealFieldElement, ())
end

function get_order(obj::DormandPrince54FieldIntegrator)
    return jcall(obj, "getOrder", jint, ())
end

function get_safety(obj::EmbeddedRungeKuttaFieldIntegrator)
    return jcall(obj, "getSafety", RealFieldElement, ())
end

function integrate(obj::EmbeddedRungeKuttaFieldIntegrator, arg0::FieldExpandableODE, arg1::FieldODEState, arg2::RealFieldElement)
    return jcall(obj, "integrate", FieldODEStateAndDerivative, (FieldExpandableODE, FieldODEState, RealFieldElement), arg0, arg1, arg2)
end

function set_max_growth(obj::EmbeddedRungeKuttaFieldIntegrator, arg0::RealFieldElement)
    return jcall(obj, "setMaxGrowth", void, (RealFieldElement,), arg0)
end

function set_min_reduction(obj::EmbeddedRungeKuttaFieldIntegrator, arg0::RealFieldElement)
    return jcall(obj, "setMinReduction", void, (RealFieldElement,), arg0)
end

function set_safety(obj::EmbeddedRungeKuttaFieldIntegrator, arg0::RealFieldElement)
    return jcall(obj, "setSafety", void, (RealFieldElement,), arg0)
end


using Base.Iterators: Iterators, product
using ..Datasets: getsavefolder, Cube

"""
    savecube(cube,name::String)

Save a [`YAXArray`](@ref) to the folder `name` in the YAXArray working directory.
"""
function savecube(c, name::AbstractString;
  chunksize = Dict(),
  max_cache = 1e8,
  backend = :zarr,
  backendargs...
)
allax = caxes(c)
if !isa(chunksize,AbstractDict) && !isa(chunksize,NamedTuple)
  @warn "Chunksize must be provided as a Dict mapping axis names to chunk size in the future"
  chunksize = OrderedDict(axname(i[1])=>i[2] for i in zip(allax,chunksize))
end
  firstaxes = sort!([findAxis(String(k),allax) for k in keys(chunksize)])
  lastaxes = setdiff(1:length(allax),firstaxes)
  allax = allax[[firstaxes;lastaxes]]
  dl = cumprod(length.(caxes(c)))
  isplit = findfirst(i->i>max_cache/sizeof(eltype(c)),dl)
  isplit isa Nothing && (isplit=length(dl)+1)
  forcesingle = (isplit+1)<length(firstaxes)
  axin = allax[1:isplit-1]
  axn = axname.(axin)
  indims = InDims(axn...)
  outdims = OutDims(axn..., backend=backend,chunksize=chunksize, path = name; backendargs...)
  function cop(xout,xin)
    xout .= xin
  end
  if forcesingle
    nprocs()>1 && println("Forcing single core processing because of bad chunk size")
    o = mapCube(cop,c,indims=indims, outdims=outdims,ispar=false,max_cache=max_cache)
  else
    o = mapCube(cop,c,indims=indims, outdims=outdims,max_cache=max_cache)
  end
end

function loadcube(s)
  Cube(getsavefolder(s, true))
end

function rmcube(s)
  p = getsavefolder(s, true)
  if isfile(p) || isdir(p)
    rm(p, recursive=true)
  end
end

function writefun(xout,xin::AbstractArray{Union{Missing,T}},a,nd,cont_loop,filename) where T

  #TODO replace -9999.0 this with a default missing value
  x = map(ix->ismissing(ix) ? convert(T,-9999.0) : ix,xin)

  count_vec = fill(-1,nd)
  start_vec = fill(1,nd)

  used_syms = Symbol[]
  for (k,v) in cont_loop
    count_vec[k] = 1
    start_vec[k] = a[Symbol(v)][1]
    push!(used_syms,Symbol(v))
  end

  splinds = Iterators.filter(i->!in(i,used_syms),keys(a))
  vn = join(string.([a[s][2] for s in splinds]),"_")
  isempty(vn) && (vn="layer")
  ncwrite(x,filename,vn,start=start_vec,count=count_vec)
end

global const projection = Dict(
"grid_mapping_name" => "transverse_mercator",
"longitude_of_central_meridian" => -9. ,
"false_easting" => 500000. ,
"false_northing" => 0. ,
"latitude_of_projection_origin" => 0. ,
"scale_factor_at_central_meridian" => 0.9996,
"long_name" => "CRS definition",
"longitude_of_prime_meridian" => 0.,
"semi_major_axis" => 6378137.,
"inverse_flattening" => 298.257223563 ,
"spatial_ref" => "PROJCS[\"WGS 84 / UTM zone 29N\",GEOGCS[\"WGS 84\",
DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,
AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],
PRIMEM[\"Greenwich\",0,AUTHORITY[\"EPSG\",\"8901\"]],
UNIT[\"degree\",0.0174532925199433,AUTHORITY[\"EPSG\",\"9122\"]],
AUTHORITY[\"EPSG\",\"4326\"]],
PROJECTION[\"Transverse_Mercator\"],
PARAMETER[\"latitude_of_origin\",0],
PARAMETER[\"central_meridian\",-9],
PARAMETER[\"scale_factor\",0.9996],
PARAMETER[\"false_easting\",500000],
PARAMETER[\"false_northing\",0],
UNIT[\"metre\",1,AUTHORITY[\"EPSG\",\"9001\"]],
AXIS[\"Easting\",EAST],
AXIS[\"Northing\",NORTH],
AUTHORITY[\"EPSG\",\"32629\"]]",
"GeoTransform" => "722576.2320803495 20 0 4115483.464603715 0 -20 ")

global const epsg4326=Dict(
  "grid_mapping_name" => "latitude_longitude",
  "long_name" => "CRS definition",
  "longitude_of_prime_meridian" => 0.,
  "semi_major_axis" => 6378137.,
  "inverse_flattening" => 298.257223563,
  "spatial_ref" => "GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433],AUTHORITY[\"EPSG\",\"4326\"]]",
  "GeoTransform" => "-98.68712696894481 0.0001796630568239077 0 20.69179551612753 0 -0.0001796630568239077 "
)