# Victoria Park using-server

using IncrementalInference
using HDF5, JLD, Gadfly, Colors, Cairo
using KernelDensityEstimate, Distributions
using Caesar, RoME
using RoMEPlotting

## load all the model data
# d = odometry information
# f = laser scanner detections
# MM = multi-modal individual id references
# MMr = reworked to map to only one previous feature
# examplefolder, datafolder

function evalLikelihood(fg::G, sym::Symbol, point::Vector{Float64}) where G <: AbstractDFG
  p = getVertKDE(fg, sym)
  Ndim(p) == length(point) ? nothing : error("point (dim=$(length(point))) must have same dimension as belief (dim=$(Ndim(p)))")
  evaluateDualTree(p, (point')')[1]
end

# Evaluate the likelihood of an Array{2} of points on the marginal belief of some variable
# note the dimensions must match
function evalLikelihood(fg::G, sym::Symbol, points::Array{Float64,2}) where G <: AbstractDFG
  p = getVertKDE(fg, sym)
  Ndim(p) == size(points,1) ? nothing : error("points (dim=$(size(points,1))) must have same dimension as belief (dim=$(Ndim(p)))")
  evaluateDualTree(p, (points))
end

include(joinpath(Pkg.dir("Caesar"),"examples","wheeled","loadVicPrkData.jl"))

include(joinpath(ENV["HOME"],"Documents","blandauthlocal.jl"))
backend_config, user_config = standardcloudgraphsetup(addrdict=addrdict)
addrdict["sessionId"] = "VICPRK_TEST"
addrdict["robotId"] = "Ute"

# Start new session

# Graphs.plot(fg.g)

fg = initfg(sessionname=user_config["sessionId"], robotname=user_config["robotId"], cloudgraph=backend_config)

# deleteServerSession!(fg.cg, user_config["session"])

# init pose
prevn = initFactorGraph!(fg, init=d[1][1:3])[1]
Podo=diagm([0.5;0.5;0.005])
N=100
lcmode=:unimodal
lsrNoise=diagm([0.1;1.0])

# # test dev code ================================================================
# idx = 2
# prev, X, nextn = getLastPose2D(fg)
# vp, fp = addOdoFG!(fg, nextn, d[idx][1:3], Podo, N=N)
# addLandmarksFactorGraph!(fg, f, idx, prevn, nextn, lcmode=lcmode, lsrNoise=lsrNoise, N=N, MM=MM)
# # ==============================================================================

#build :# poses for the factorGraph
for idx=2:5
  prev, X, nextn = getLastPose2D(fg)
  vp, fp = addOdoFG!(fg, nextn, d[idx][1:3], Podo, N=N)
  # add landmarks
  addLandmarksFactorGraph!(fg, f, idx, prevn, nextn, lcmode=lcmode, lsrNoise=lsrNoise, N=N, MM=MM)
  prevn = nextn
  # if (idx%10==0)
  #    Solve
  #    tree = prepBatchTree!(fg, drawpdf=true);
  #   @time inferOverTree!(fg,tree, N=100);
  # end
end

pl=drawPosesLandms(fg);
Gadfly.draw(PDF("/tmp/before.pdf",20cm,20cm),pl)

# batchSolve(fg)
# on ssh terminal, run slamindb(iterations=1)

# fetch a local copy

fg = initfg(sessionname=user_config["sessionId"], cloudgraph=backend_config) #, robotname=user_config["robotId"]
fullLocalGraphCopy!(fg)
pl1=drawPosesLandms(fg)

Gadfly.draw(PDF("/tmp/after.pdf",20cm,20cm),pl1)

@async run(`evince /tmp/after.pdf`)

subLocalGraphCopy!(fg, string.([:x1,:x2,:x3,:x4,:x5]), neighbors=1)

X,L = ls(fg)
d=0
t=[]
z=zeros(length(L)+1,length(L)+1)

j=1;
for l1 in L
  i=1
  for l2 in L
    b=getKDEMax(getVertKDE(fg, l1))
    c=evalLikelihood(fg, l2 , b)
    if ( c>0.002 && l1!=l2 )
      d+=1
      #Replace this with A delete and replace
      #what the hell should i do now
      addSoftEqualityPoint2D(fg, l1, l2 )

      push!(t,(l1,l2))
      z[i,j]=1
    elseif (l1==l2)
      z[i,j]=2
    end
    i+=1
  end
  j+=1
end
#run Server Code
fg = initfg(sessionname=user_config["session"], cloudgraph=backend_config)
fullLocalGraphCopy!(fg)
pl2=drawPosesLandms(fg)
draw(PDF("daniel.pdf",20cm,20cm),pl2)

# Remove the new session from the server
deleteServerSession!(fg.cg, user_config["session"])

#


test_path = mktempdir()
@testset "ThermalGen data misspecification" begin
    # See https://discourse.julialang.org/t/how-to-use-test-warn/15557/5 about testing for warning throwing
    warn_message = "The data doesn't include devices of type ThermalStandard, consider changing the device models"
    model = DeviceModel(ThermalStandard, ThermalStandardUnitCommitment)
    c_sys5_re_only = PSB.build_system(PSITestSystems, "c_sys5_re_only")
    op_problem = OperationsProblem(MockOperationProblem, DCPPowerModel, c_sys5_re_only)
    @test_logs (:info,) (:warn, warn_message) match_mode = :any mock_construct_device!(
        op_problem,
        model,
    )
end

################################### Unit Commitment tests ##################################
@testset "Thermal UC With DC - PF" begin
    bin_variable_names = [
        PSI.make_variable_name(PSI.ON, PSY.ThermalStandard),
        PSI.make_variable_name(PSI.START, PSY.ThermalStandard),
        PSI.make_variable_name(PSI.STOP, PSY.ThermalStandard),
    ]
    uc_constraint_names = [
        PSI.make_constraint_name(PSI.RAMP_UP, PSY.ThermalStandard),
        PSI.make_constraint_name(PSI.RAMP_DOWN, PSY.ThermalStandard),
        PSI.make_constraint_name(PSI.DURATION_UP, PSY.ThermalStandard),
        PSI.make_constraint_name(PSI.DURATION_DOWN, PSY.ThermalStandard),
    ]
    model = DeviceModel(ThermalStandard, ThermalStandardUnitCommitment)

    c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc")
    op_problem = OperationsProblem(MockOperationProblem, DCPPowerModel, c_sys5_uc)
    mock_construct_device!(op_problem, model)
    moi_tests(op_problem, false, 480, 0, 480, 120, 120, true)
    psi_constraint_test(op_problem, uc_constraint_names)
    psi_checkbinvar_test(op_problem, bin_variable_names)
    psi_checkobjfun_test(op_problem, GAEVF)

    op_problem = OperationsProblem(
        MockOperationProblem,
        DCPPowerModel,
        c_sys5_uc;
        use_parameters = true,
    )
    mock_construct_device!(op_problem, model)
    moi_tests(op_problem, true, 480, 0, 480, 120, 120, true)
    psi_constraint_test(op_problem, uc_constraint_names)
    psi_checkbinvar_test(op_problem, bin_variable_names)
    psi_checkobjfun_test(op_problem, GAEVF)

    c_sys14 = PSB.build_system(PSITestSystems, "c_sys14")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            DCPPowerModel,
            c_sys14;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 480, 0, 240, 120, 120, true)
        psi_checkbinvar_test(op_problem, bin_variable_names)
        psi_checkobjfun_test(op_problem, GQEVF)
    end
end

@testset "Thermal UC With AC - PF" begin
    bin_variable_names = [
        PSI.make_variable_name(PSI.ON, PSY.ThermalStandard),
        PSI.make_variable_name(PSI.START, PSY.ThermalStandard),
        PSI.make_variable_name(PSI.STOP, PSY.ThermalStandard),
    ]
    uc_constraint_names = [
        PSI.make_constraint_name(PSI.RAMP_UP, PSY.ThermalStandard),
        PSI.make_constraint_name(PSI.RAMP_DOWN, PSY.ThermalStandard),
        PSI.make_constraint_name(PSI.DURATION_UP, PSY.ThermalStandard),
        PSI.make_constraint_name(PSI.DURATION_DOWN, PSY.ThermalStandard),
    ]
    model = DeviceModel(ThermalStandard, ThermalStandardUnitCommitment)

    c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc")
    op_problem = OperationsProblem(MockOperationProblem, ACPPowerModel, c_sys5_uc)
    mock_construct_device!(op_problem, model)
    moi_tests(op_problem, false, 600, 0, 600, 240, 120, true)
    psi_constraint_test(op_problem, uc_constraint_names)
    psi_checkbinvar_test(op_problem, bin_variable_names)
    psi_checkobjfun_test(op_problem, GAEVF)

    op_problem = OperationsProblem(
        MockOperationProblem,
        ACPPowerModel,
        c_sys5_uc;
        use_parameters = true,
    )
    mock_construct_device!(op_problem, model)
    moi_tests(op_problem, true, 600, 0, 600, 240, 120, true)
    psi_constraint_test(op_problem, uc_constraint_names)
    psi_checkbinvar_test(op_problem, bin_variable_names)
    psi_checkobjfun_test(op_problem, GAEVF)

    c_sys14 = PSB.build_system(PSITestSystems, "c_sys14")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            ACPPowerModel,
            c_sys14;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 600, 0, 360, 240, 120, true)
        psi_checkbinvar_test(op_problem, bin_variable_names)
        psi_checkobjfun_test(op_problem, GQEVF)
    end
end

@testset "Thermal MultiStart UC With DC - PF" begin
    bin_variable_names = [
        PSI.make_variable_name(PSI.ON, PSY.ThermalMultiStart),
        PSI.make_variable_name(PSI.START, PSY.ThermalMultiStart),
        PSI.make_variable_name(PSI.STOP, PSY.ThermalMultiStart),
    ]
    uc_constraint_names = [
        PSI.make_constraint_name(PSI.RAMP_UP, PSY.ThermalMultiStart),
        PSI.make_constraint_name(PSI.RAMP_DOWN, PSY.ThermalMultiStart),
        PSI.make_constraint_name(PSI.DURATION_UP, PSY.ThermalMultiStart),
        PSI.make_constraint_name(PSI.DURATION_DOWN, PSY.ThermalMultiStart),
    ]
    model = DeviceModel(ThermalMultiStart, ThermalStandardUnitCommitment)

    c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_pglib")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            DCPPowerModel,
            c_sys5_uc;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 384, 0, 240, 48, 96, true)
        psi_constraint_test(op_problem, uc_constraint_names)
        psi_checkbinvar_test(op_problem, bin_variable_names)
        psi_checkobjfun_test(op_problem, GAEVF)
    end
end

@testset "Thermal MultiStart UC With AC - PF" begin
    bin_variable_names = [
        PSI.make_variable_name(PSI.ON, PSY.ThermalMultiStart),
        PSI.make_variable_name(PSI.START, PSY.ThermalMultiStart),
        PSI.make_variable_name(PSI.STOP, PSY.ThermalMultiStart),
    ]
    uc_constraint_names = [
        PSI.make_constraint_name(PSI.RAMP_UP, PSY.ThermalMultiStart),
        PSI.make_constraint_name(PSI.RAMP_DOWN, PSY.ThermalMultiStart),
        PSI.make_constraint_name(PSI.DURATION_UP, PSY.ThermalMultiStart),
        PSI.make_constraint_name(PSI.DURATION_DOWN, PSY.ThermalMultiStart),
    ]
    model = DeviceModel(ThermalMultiStart, ThermalStandardUnitCommitment)

    c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_pglib")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            ACPPowerModel,
            c_sys5_uc;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 432, 0, 288, 96, 96, true)
        psi_constraint_test(op_problem, uc_constraint_names)
        psi_checkbinvar_test(op_problem, bin_variable_names)
        psi_checkobjfun_test(op_problem, GAEVF)
    end
end

################################### Basic Unit Commitment tests ############################
@testset "Thermal Basic UC With DC - PF" begin
    bin_variable_names = [
        PSI.make_variable_name(PSI.ON, PSY.ThermalStandard),
        PSI.make_variable_name(PSI.START, PSY.ThermalStandard),
        PSI.make_variable_name(PSI.STOP, PSY.ThermalStandard),
    ]
    model = DeviceModel(ThermalStandard, ThermalBasicUnitCommitment)

    c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc")
    op_problem = OperationsProblem(MockOperationProblem, DCPPowerModel, c_sys5_uc)
    mock_construct_device!(op_problem, model)
    moi_tests(op_problem, false, 480, 0, 240, 120, 120, true)
    psi_checkbinvar_test(op_problem, bin_variable_names)
    psi_checkobjfun_test(op_problem, GAEVF)

    op_problem = OperationsProblem(
        MockOperationProblem,
        DCPPowerModel,
        c_sys5_uc;
        use_parameters = true,
    )
    mock_construct_device!(op_problem, model)
    moi_tests(op_problem, true, 480, 0, 240, 120, 120, true)
    psi_checkbinvar_test(op_problem, bin_variable_names)
    psi_checkobjfun_test(op_problem, GAEVF)

    c_sys14 = PSB.build_system(PSITestSystems, "c_sys14")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            DCPPowerModel,
            c_sys14;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 480, 0, 240, 120, 120, true)
        psi_checkbinvar_test(op_problem, bin_variable_names)
        psi_checkobjfun_test(op_problem, GQEVF)
    end
end

@testset "Thermal Basic UC With AC - PF" begin
    bin_variable_names = [
        PSI.make_variable_name(PSI.ON, PSY.ThermalStandard),
        PSI.make_variable_name(PSI.START, PSY.ThermalStandard),
        PSI.make_variable_name(PSI.STOP, PSY.ThermalStandard),
    ]
    model = DeviceModel(ThermalStandard, ThermalBasicUnitCommitment)

    c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc")
    op_problem = OperationsProblem(MockOperationProblem, ACPPowerModel, c_sys5_uc)
    mock_construct_device!(op_problem, model)
    moi_tests(op_problem, false, 600, 0, 360, 240, 120, true)
    psi_checkbinvar_test(op_problem, bin_variable_names)
    psi_checkobjfun_test(op_problem, GAEVF)

    op_problem = OperationsProblem(
        MockOperationProblem,
        ACPPowerModel,
        c_sys5_uc;
        use_parameters = true,
    )
    mock_construct_device!(op_problem, model)
    moi_tests(op_problem, true, 600, 0, 360, 240, 120, true)
    psi_checkbinvar_test(op_problem, bin_variable_names)
    psi_checkobjfun_test(op_problem, GAEVF)

    c_sys14 = PSB.build_system(PSITestSystems, "c_sys14")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            ACPPowerModel,
            c_sys14;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 600, 0, 360, 240, 120, true)
        psi_checkbinvar_test(op_problem, bin_variable_names)
        psi_checkobjfun_test(op_problem, GQEVF)
    end
end

@testset "Thermal MultiStart Basic UC With DC - PF" begin
    bin_variable_names = [
        PSI.make_variable_name(PSI.ON, PSY.ThermalMultiStart),
        PSI.make_variable_name(PSI.START, PSY.ThermalMultiStart),
        PSI.make_variable_name(PSI.STOP, PSY.ThermalMultiStart),
    ]
    model = DeviceModel(ThermalMultiStart, ThermalBasicUnitCommitment)

    c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_pglib")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            DCPPowerModel,
            c_sys5_uc;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 384, 0, 96, 48, 96, true)
        psi_checkbinvar_test(op_problem, bin_variable_names)
        psi_checkobjfun_test(op_problem, GAEVF)
    end
end

@testset "Thermal MultiStart Basic UC With AC - PF" begin
    bin_variable_names = [
        PSI.make_variable_name(PSI.ON, PSY.ThermalMultiStart),
        PSI.make_variable_name(PSI.START, PSY.ThermalMultiStart),
        PSI.make_variable_name(PSI.STOP, PSY.ThermalMultiStart),
    ]
    model = DeviceModel(ThermalMultiStart, ThermalBasicUnitCommitment)

    c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_pglib")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            ACPPowerModel,
            c_sys5_uc;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 432, 0, 144, 96, 96, true)
        psi_checkbinvar_test(op_problem, bin_variable_names)
        psi_checkobjfun_test(op_problem, GAEVF)
    end
end

################################### Basic Dispatch tests ###################################
@testset "Thermal Dispatch With DC - PF" begin
    model = DeviceModel(ThermalStandard, ThermalDispatch)
    c_sys5 = PSB.build_system(PSITestSystems, "c_sys5")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            DCPPowerModel,
            c_sys5;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 120, 0, 120, 120, 0, false)
        psi_checkobjfun_test(op_problem, GAEVF)
    end

    c_sys14 = PSB.build_system(PSITestSystems, "c_sys14")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            DCPPowerModel,
            c_sys14;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 120, 0, 120, 120, 0, false)
        psi_checkobjfun_test(op_problem, GQEVF)
    end
end

@testset "Thermal Dispatch With AC - PF" begin
    model = DeviceModel(ThermalStandard, ThermalDispatch)
    c_sys5 = PSB.build_system(PSITestSystems, "c_sys5")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            ACPPowerModel,
            c_sys5;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 240, 0, 240, 240, 0, false)
        psi_checkobjfun_test(op_problem, GAEVF)
    end

    c_sys14 = PSB.build_system(PSITestSystems, "c_sys14")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            ACPPowerModel,
            c_sys14;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 240, 0, 240, 240, 0, false)
        psi_checkobjfun_test(op_problem, GQEVF)
    end
end

# This Formulation is currently broken
@testset "ThermalMultiStart Dispatch With DC - PF" begin
    model = DeviceModel(ThermalMultiStart, ThermalDispatch)
    c_sys5 = PSB.build_system(PSITestSystems, "c_sys5_pglib")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            DCPPowerModel,
            c_sys5;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 240, 0, 48, 48, 48, false)
        psi_checkobjfun_test(op_problem, GAEVF)
    end
end

@testset "ThermalMultiStart Dispatch With AC - PF" begin
    model = DeviceModel(ThermalMultiStart, ThermalDispatch)
    c_sys5 = PSB.build_system(PSITestSystems, "c_sys5_pglib")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            ACPPowerModel,
            c_sys5;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 288, 0, 96, 96, 48, false)
        psi_checkobjfun_test(op_problem, GAEVF)
    end
end

################################### No Minimum Dispatch tests ##############################

@testset "Thermal Dispatch NoMin With DC - PF" begin
    model = DeviceModel(ThermalStandard, ThermalDispatchNoMin)
    c_sys5 = PSB.build_system(PSITestSystems, "c_sys5")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            DCPPowerModel,
            c_sys5;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 120, 0, 120, 120, 0, false)
        moi_lbvalue_test(op_problem, :P_lb__ThermalStandard__RangeConstraint, 0.0)
        psi_checkobjfun_test(op_problem, GAEVF)
    end

    c_sys14 = PSB.build_system(PSITestSystems, "c_sys14")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            DCPPowerModel,
            c_sys14;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 120, 0, 120, 120, 0, false)
        moi_lbvalue_test(op_problem, :P_lb__ThermalStandard__RangeConstraint, 0.0)
        psi_checkobjfun_test(op_problem, GQEVF)
    end
end

@testset "Thermal Dispatch NoMin With AC - PF" begin
    model = DeviceModel(ThermalStandard, ThermalDispatchNoMin)
    c_sys5 = PSB.build_system(PSITestSystems, "c_sys5")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            ACPPowerModel,
            c_sys5;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 240, 0, 240, 240, 0, false)
        moi_lbvalue_test(op_problem, :P_lb__ThermalStandard__RangeConstraint, 0.0)
        psi_checkobjfun_test(op_problem, GAEVF)
    end

    c_sys14 = PSB.build_system(PSITestSystems, "c_sys14")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            ACPPowerModel,
            c_sys14;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 240, 0, 240, 240, 0, false)
        moi_lbvalue_test(op_problem, :P_lb__ThermalStandard__RangeConstraint, 0.0)
        psi_checkobjfun_test(op_problem, GQEVF)
    end
end

# This Formulation is currently broken
#=
@testset "Thermal Dispatch NoMin With DC - PF" begin
    model = DeviceModel(ThermalMultiStart, ThermalDispatchNoMin)
    c_sys5 = PSB.build_system(PSITestSystems, "c_sys5_pglib")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            DCPPowerModel,
            c_sys5;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 240, 0, 48, 48, 48, false)
        moi_lbvalue_test(op_problem, :P_lb__ThermalMultiStart__RangeConstraint, 0.0)
        psi_checkobjfun_test(op_problem, GAEVF)
    end
end

@testset "ThermalMultiStart Dispatch NoMin With AC - PF" begin
    model = DeviceModel(ThermalMultiStart, ThermalDispatchNoMin)
    c_sys5 = PSB.build_system(PSITestSystems, "c_sys5_pglib")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            ACPPowerModel,
            c_sys5;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 288, 0, 96, 96, 48, false)
        moi_lbvalue_test(op_problem, :P_lb__ThermalMultiStart__RangeConstraint, 0.0)
        psi_checkobjfun_test(op_problem, GAEVF)
    end
end
=#
################################### Ramp Limited Testing ##################################
@testset "Thermal Ramp Limited Dispatch With DC - PF" begin
    constraint_names = [
        PSI.make_constraint_name(PSI.RAMP_UP, PSY.ThermalStandard),
        PSI.make_constraint_name(PSI.RAMP_DOWN, PSY.ThermalStandard),
    ]
    model = DeviceModel(ThermalStandard, ThermalRampLimited)
    c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            DCPPowerModel,
            c_sys5_uc;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 120, 0, 216, 120, 0, false)
        psi_constraint_test(op_problem, constraint_names)
        psi_checkobjfun_test(op_problem, GAEVF)
    end

    c_sys14 = PSB.build_system(PSITestSystems, "c_sys14")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            DCPPowerModel,
            c_sys14;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 120, 0, 120, 120, 0, false)
        psi_checkobjfun_test(op_problem, GQEVF)
    end
end

@testset "Thermal Ramp Limited Dispatch With AC - PF" begin
    constraint_names = [
        PSI.make_constraint_name(PSI.RAMP_UP, PSY.ThermalStandard),
        PSI.make_constraint_name(PSI.RAMP_DOWN, PSY.ThermalStandard),
    ]
    model = DeviceModel(ThermalStandard, ThermalRampLimited)
    c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            ACPPowerModel,
            c_sys5_uc;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 240, 0, 336, 240, 0, false)
        psi_constraint_test(op_problem, constraint_names)
        psi_checkobjfun_test(op_problem, GAEVF)
    end

    c_sys14 = PSB.build_system(PSITestSystems, "c_sys14")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            ACPPowerModel,
            c_sys14;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 240, 0, 240, 240, 0, false)
        psi_checkobjfun_test(op_problem, GQEVF)
    end
end

@testset "Thermal Ramp Limited Dispatch With DC - PF" begin
    constraint_names = [
        PSI.make_constraint_name(PSI.RAMP_UP, PSY.ThermalMultiStart),
        PSI.make_constraint_name(PSI.RAMP_DOWN, PSY.ThermalMultiStart),
    ]
    model = DeviceModel(ThermalMultiStart, ThermalRampLimited)
    c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_pglib")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            DCPPowerModel,
            c_sys5_uc;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 240, 0, 144, 48, 48, false)
        psi_constraint_test(op_problem, constraint_names)
        psi_checkobjfun_test(op_problem, GAEVF)
    end
end

@testset "Thermal Ramp Limited Dispatch With AC - PF" begin
    constraint_names = [
        PSI.make_constraint_name(PSI.RAMP_UP, PSY.ThermalMultiStart),
        PSI.make_constraint_name(PSI.RAMP_DOWN, PSY.ThermalMultiStart),
    ]
    model = DeviceModel(ThermalMultiStart, ThermalRampLimited)
    c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_pglib")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            ACPPowerModel,
            c_sys5_uc;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 288, 0, 192, 96, 48, false)
        psi_constraint_test(op_problem, constraint_names)
        psi_checkobjfun_test(op_problem, GAEVF)
    end
end

################################### ThermalMultiStart Testing ##################################

@testset "Thermal MultiStart with MultiStart UC and DC - PF" begin
    constraint_names = [
        PSI.make_constraint_name(PSI.ACTIVE_RANGE_IC, PSY.ThermalMultiStart),
        PSI.make_constraint_name(PSI.START_TYPE, PSY.ThermalMultiStart),
        PSI.make_constraint_name(PSI.MUST_RUN_LB, PSY.ThermalMultiStart),
        PSI.make_constraint_name(PSI.STARTUP_TIMELIMIT_WARM, PSY.ThermalMultiStart),
        PSI.make_constraint_name(PSI.STARTUP_TIMELIMIT_HOT, PSY.ThermalMultiStart),
        PSI.make_constraint_name(PSI.STARTUP_INITIAL_CONDITION_LB, PSY.ThermalMultiStart),
        PSI.make_constraint_name(PSI.STARTUP_INITIAL_CONDITION_UB, PSY.ThermalMultiStart),
    ]
    model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalMultiStartUnitCommitment)
    no_less_than = Dict(true => 334, false => 330)
    c_sys5_pglib = PSB.build_system(PSITestSystems, "c_sys5_pglib")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            DCPPowerModel,
            c_sys5_pglib;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 528, 0, no_less_than[p], 60, 144, true)
        psi_constraint_test(op_problem, constraint_names)
        psi_checkobjfun_test(op_problem, GAEVF)
    end
end

@testset "Thermal MultiStart with MultiStart UC and AC - PF" begin
    constraint_names = [
        PSI.make_constraint_name(PSI.ACTIVE_RANGE_IC, PSY.ThermalMultiStart),
        PSI.make_constraint_name(PSI.START_TYPE, PSY.ThermalMultiStart),
        PSI.make_constraint_name(PSI.MUST_RUN_LB, PSY.ThermalMultiStart),
        PSI.make_constraint_name(PSI.STARTUP_TIMELIMIT_WARM, PSY.ThermalMultiStart),
        PSI.make_constraint_name(PSI.STARTUP_TIMELIMIT_HOT, PSY.ThermalMultiStart),
        PSI.make_constraint_name(PSI.STARTUP_INITIAL_CONDITION_LB, PSY.ThermalMultiStart),
        PSI.make_constraint_name(PSI.STARTUP_INITIAL_CONDITION_UB, PSY.ThermalMultiStart),
    ]
    model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalMultiStartUnitCommitment)
    no_less_than = Dict(true => 382, false => 378)
    c_sys5_pglib = PSB.build_system(PSITestSystems, "c_sys5_pglib")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            ACPPowerModel,
            c_sys5_pglib;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 576, 0, no_less_than[p], 108, 144, true)
        psi_constraint_test(op_problem, constraint_names)
        psi_checkobjfun_test(op_problem, GAEVF)
    end
end

################################### Thermal Compact UC Testing ##################################
@testset "Thermal Standard with Compact UC and DC - PF" begin
    model = DeviceModel(PSY.ThermalStandard, PSI.ThermalCompactUnitCommitment)
    c_sys5 = PSB.build_system(PSITestSystems, "c_sys5")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            DCPPowerModel,
            c_sys5;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 480, 0, 595, 0, 120, true)
        psi_checkobjfun_test(op_problem, GAEVF)
    end
end

@testset "Thermal MultiStart with Compact UC and DC - PF" begin
    model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalCompactUnitCommitment)
    c_sys5_pglib = PSB.build_system(PSITestSystems, "c_sys5_pglib")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            DCPPowerModel,
            c_sys5_pglib;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 384, 0, 286, 0, 96, true)
        psi_checkobjfun_test(op_problem, GAEVF)
    end
end

@testset "Thermal Standard with Compact UC and AC - PF" begin
    model = DeviceModel(PSY.ThermalStandard, PSI.ThermalCompactUnitCommitment)
    c_sys5 = PSB.build_system(PSITestSystems, "c_sys5")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            ACPPowerModel,
            c_sys5;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 600, 0, 715, 120, 120, true)
        psi_checkobjfun_test(op_problem, GAEVF)
    end
end

@testset "Thermal MultiStart with Compact UC and AC - PF" begin
    model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalCompactUnitCommitment)
    c_sys5_pglib = PSB.build_system(PSITestSystems, "c_sys5_pglib")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            ACPPowerModel,
            c_sys5_pglib;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 432, 0, 334, 48, 96, true)
        psi_checkobjfun_test(op_problem, GAEVF)
    end
end

################################### Thermal Compact Dispatch Testing ##################################

@testset "Thermal Standard with Compact Dispatch and DC - PF" begin
    model = DeviceModel(PSY.ThermalStandard, PSI.ThermalCompactDispatch)
    c_sys5 = PSB.build_system(PSITestSystems, "c_sys5")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            DCPPowerModel,
            c_sys5;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 120, 0, 168, 120, 0, false)
        psi_checkobjfun_test(op_problem, GAEVF)
    end
end

@testset "Thermal MultiStart with Compact Dispatch and DC - PF" begin
    model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalCompactDispatch)
    c_sys5_pglib = PSB.build_system(PSITestSystems, "c_sys5_pglib")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            DCPPowerModel,
            c_sys5_pglib;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 240, 0, 144, 48, 48, false)
        psi_checkobjfun_test(op_problem, GAEVF)
    end
end

@testset "Thermal Standard with Compact Dispatch and AC - PF" begin
    model = DeviceModel(PSY.ThermalStandard, PSI.ThermalCompactDispatch)
    c_sys5 = PSB.build_system(PSITestSystems, "c_sys5")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            ACPPowerModel,
            c_sys5;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 240, 0, 288, 240, 0, false)
        psi_checkobjfun_test(op_problem, GAEVF)
    end
end

@testset "Thermal MultiStart with Compact Dispatch and AC - PF" begin
    model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalCompactDispatch)
    no_less_than = Dict(true => 382, false => 378)
    c_sys5_pglib = PSB.build_system(PSITestSystems, "c_sys5_pglib")
    for p in [true, false]
        op_problem = OperationsProblem(
            MockOperationProblem,
            ACPPowerModel,
            c_sys5_pglib;
            use_parameters = p,
        )
        mock_construct_device!(op_problem, model)
        moi_tests(op_problem, p, 288, 0, 192, 96, 48, false)
        psi_checkobjfun_test(op_problem, GAEVF)
    end
end

############################# Model validation tests #######################################
@testset "Solving ED with CopperPlate for testing Ramping Constraints" begin
    ramp_test_sys = PSB.build_system(PSITestSystems, "c_ramp_test")
    template = OperationsProblemTemplate(CopperPlatePowerModel)
    set_device_model!(template, ThermalStandard, ThermalRampLimited)
    set_device_model!(template, PowerLoad, StaticPowerLoad)
    ED = OperationsProblem(
        EconomicDispatchProblem,
        template,
        ramp_test_sys;
        optimizer = Cbc_optimizer,
    )
    @test build!(ED; output_dir = mktempdir(cleanup = true)) == PSI.BuildStatus.BUILT
    moi_tests(ED, false, 10, 0, 20, 10, 5, false)
    res = solve!(ED)
    psi_checksolve_test(ED, [MOI.OPTIMAL], 11191.00)
end

# Testing Duration Constraints
@testset "Solving UC with CopperPlate for testing Duration Constraints" begin
    template = get_thermal_standard_uc_template()
    UC = OperationsProblem(
        UnitCommitmentProblem,
        template,
        PSB.build_system(PSITestSystems, "c_duration_test");
        optimizer = Cbc_optimizer,
        use_parameters = true,
    )
    @test build!(UC; output_dir = mktempdir(cleanup = true)) == PSI.BuildStatus.BUILT
    moi_tests(UC, true, 56, 0, 56, 14, 21, true)
    psi_checksolve_test(UC, [MOI.OPTIMAL], 8223.50)
end

## PWL linear Cost implementation test
@testset "Solving UC with CopperPlate testing Convex PWL" begin
    template = get_thermal_standard_uc_template()
    UC = OperationsProblem(
        UnitCommitmentProblem,
        template,
        PSB.build_system(PSITestSystems, "c_linear_pwl_test");
        optimizer = Cbc_optimizer,
        use_parameters = true,
    )
    @test build!(UC; output_dir = mktempdir(cleanup = true)) == PSI.BuildStatus.BUILT
    moi_tests(UC, true, 32, 0, 8, 4, 10, true)
    psi_checksolve_test(UC, [MOI.OPTIMAL], 9336.736919354838)
end

@testset "Solving UC with CopperPlate testing PWL-SOS2 implementation" begin
    template = get_thermal_standard_uc_template()
    UC = OperationsProblem(
        UnitCommitmentProblem,
        template,
        PSB.build_system(PSITestSystems, "c_sos_pwl_test");
        optimizer = Cbc_optimizer,
        use_parameters = true,
    )
    @test build!(UC; output_dir = mktempdir(cleanup = true)) == PSI.BuildStatus.BUILT
    moi_tests(UC, true, 32, 0, 8, 4, 14, true)
    psi_checksolve_test(UC, [MOI.OPTIMAL], 8500.89716, 10.0)
end

@testset "UC with MarketBid Cost in ThermalGenerators" begin
    template = get_thermal_standard_uc_template()
    set_device_model!(
        template,
        DeviceModel(ThermalMultiStart, ThermalMultiStartUnitCommitment),
    )
    UC = OperationsProblem(
        UnitCommitmentProblem,
        template,
        PSB.build_system(PSITestSystems, "c_market_bid_cost");
        optimizer = Cbc_optimizer,
        use_parameters = true,
    )
    @test build!(UC; output_dir = mktempdir(cleanup = true)) == PSI.BuildStatus.BUILT
    moi_tests(UC, true, 38, 0, 18, 8, 13, true)
end

@testset "Operation ModelThermalDispatchNoMin - and PWL Non Convex" begin
    c_sys5_pwl_ed_nonconvex = PSB.build_system(PSITestSystems, "c_sys5_pwl_ed_nonconvex")
    template = get_thermal_dispatch_template_network()
    set_device_model!(template, DeviceModel(ThermalStandard, ThermalDispatchNoMin))
    op_problem = OperationsProblem(
        MockOperationProblem,
        CopperPlatePowerModel,
        c_sys5_pwl_ed_nonconvex;
        use_parameters = true,
        export_pwl_vars = true,
    )
    @test_throws IS.InvalidValue mock_construct_device!(
        op_problem,
        DeviceModel(ThermalStandard, ThermalDispatchNoMin),
    )
end

#TODO: Add test for newer UC models
@testset "Solving UC Models with Linear Networks" begin
    c_sys5 = PSB.build_system(PSITestSystems, "c_sys5")
    c_sys5_dc = PSB.build_system(PSITestSystems, "c_sys5_dc")
    parameters_value = [true, false]
    systems = [c_sys5, c_sys5_dc]
    networks = [DCPPowerModel, NFAPowerModel, StandardPTDFModel, CopperPlatePowerModel]
    PTDF_ref = IdDict{System, PTDF}(c_sys5 => PTDF(c_sys5), c_sys5_dc => PTDF(c_sys5_dc))

    for net in networks, p in parameters_value, sys in systems
        template = get_thermal_dispatch_template_network(net)
        set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment)
        UC = OperationsProblem(
            template,
            sys;
            optimizer = GLPK_optimizer,
            use_parameters = p,
            PTDF = PTDF_ref[sys],
        )
        @test build!(UC; output_dir = mktempdir(cleanup = true)) == PSI.BuildStatus.BUILT
        psi_checksolve_test(UC, [MOI.OPTIMAL, MOI.LOCALLY_SOLVED], 340000, 100000)
    end
end


module General

using ..TypeSynonyms

struct Contour
    center    ::RationalComplex
    radius    ::Float
    max_power ::Int
end
export Contour

abstract type ProblemType end
export ProblemType

abstract type Fiber{P <: ProblemType} end
export Fiber

abstract type Cohesive{P <: ProblemType} end
export Cohesive

abstract type SubMatrixType{P <: ProblemType} end
export SubMatrixType

struct SubMatrix_Zero{P <: ProblemType} <: SubMatrixType{P}
    contour ::Contour
end
export SubMatrix_Zero

struct SubMatrix_Fiber{P <: ProblemType} <: SubMatrixType{P}
    contour ::Contour
    fiber   ::Fiber{P}
end
export SubMatrix_Fiber

struct SubMatrix_Cohesive{P <: ProblemType} <: SubMatrixType{P}
    contour  ::Contour
    cohesive ::Cohesive{P}
end
export SubMatrix_Cohesive

abstract type SubMatrix_Averaging{P <: ProblemType} <: SubMatrixType{P} end
export SubMatrix_Averaging

struct ReferenceMatrix{P <: ProblemType}
    matrix ::Matrix{ SubMatrixType{P} }
end
export ReferenceMatrix

end # module General


#------------------
# Sum-Product Rules
#------------------

mutable struct SPTransitionMixtureOutNCCPX <: SumProductRule{TransitionMixture} end
outboundType(::Type{SPTransitionMixtureOutNCCPX}) = Message{Categorical}
function isApplicable(::Type{SPTransitionMixtureOutNCCPX}, input_types::Vector{<:Type})
    for (i, input_type) in enumerate(input_types)
        if (i == 1)
            (input_type == Nothing) || return false
        elseif (i == 2)
            matches(input_type, Message{Categorical}) || return false
        elseif (i == 3)
            matches(input_type, Message{Categorical}) || return false
        else
            matches(input_type, Message{PointMass}) || return false
        end
    end
    return true
end

mutable struct SPTransitionMixtureIn1CNCPX <: SumProductRule{TransitionMixture} end
outboundType(::Type{SPTransitionMixtureIn1CNCPX}) = Message{Categorical}
function isApplicable(::Type{SPTransitionMixtureIn1CNCPX}, input_types::Vector{<:Type})
    for (i, input_type) in enumerate(input_types)
        if (i == 2)
            (input_type == Nothing) || return false
        elseif (i == 1)
            matches(input_type, Message{Categorical}) || return false
        elseif (i == 3)
            matches(input_type, Message{Categorical}) || return false
        else
            matches(input_type, Message{PointMass}) || return false
        end
    end
    return true
end

mutable struct SPTransitionMixtureZCCNPX <: SumProductRule{TransitionMixture} end
outboundType(::Type{SPTransitionMixtureZCCNPX}) = Message{Categorical}
function isApplicable(::Type{SPTransitionMixtureZCCNPX}, input_types::Vector{<:Type})
    for (i, input_type) in enumerate(input_types)
        if (i == 3)
            (input_type == Nothing) || return false
        elseif (i == 1)
            matches(input_type, Message{Categorical}) || return false
        elseif (i == 2)
            matches(input_type, Message{Categorical}) || return false
        else
            matches(input_type, Message{PointMass}) || return false
        end
    end
    return true
end

#-----------------
# Structured Rules
#-----------------

mutable struct SVBTransitionMixtureOutNCCDX <: StructuredVariationalRule{TransitionMixture} end
outboundType(::Type{SVBTransitionMixtureOutNCCDX}) = Message{Categorical}
function isApplicable(::Type{SVBTransitionMixtureOutNCCDX}, input_types::Vector{<:Type})
    for (i, input_type) in enumerate(input_types)
        if (i == 1)
            (input_type == Nothing) || return false
        elseif (i == 2)
            matches(input_type, Message{Categorical}) || return false
        elseif (i == 3)
            matches(input_type, Message{Categorical}) || return false
        else
            matches(input_type, ProbabilityDistribution) || return false
        end
    end
    return true
end

mutable struct SVBTransitionMixtureIn1CNCDX <: StructuredVariationalRule{TransitionMixture} end
outboundType(::Type{SVBTransitionMixtureIn1CNCDX}) = Message{Categorical}
function isApplicable(::Type{SVBTransitionMixtureIn1CNCDX}, input_types::Vector{<:Type})
    for (i, input_type) in enumerate(input_types)
        if (i == 2)
            (input_type == Nothing) || return false
        elseif (i == 1)
            matches(input_type, Message{Categorical}) || return false
        elseif (i == 3)
            matches(input_type, Message{Categorical}) || return false
        else
            matches(input_type, ProbabilityDistribution) || return false
        end
    end
    return true
end

mutable struct SVBTransitionMixtureZCCNDX <: StructuredVariationalRule{TransitionMixture} end
outboundType(::Type{SVBTransitionMixtureZCCNDX}) = Message{Categorical}
function isApplicable(::Type{SVBTransitionMixtureZCCNDX}, input_types::Vector{<:Type})
    for (i, input_type) in enumerate(input_types)
        if (i == 3)
            (input_type == Nothing) || return false
        elseif (i == 1)
            matches(input_type, Message{Categorical}) || return false
        elseif (i == 2)
            matches(input_type, Message{Categorical}) || return false
        else
            matches(input_type, ProbabilityDistribution) || return false
        end
    end
    return true
end

mutable struct SVBTransitionMixtureA <: StructuredVariationalRule{TransitionMixture} end
outboundType(::Type{SVBTransitionMixtureA}) = Message{Dirichlet}
function isApplicable(::Type{SVBTransitionMixtureA}, input_types::Vector{<:Type})
    n_inputs = length(input_types)

    (Nothing_positions, p_positions) = matchPVInputs(input_types)
    n_Nothings = length(Nothing_positions)
    n_ps = length(p_positions)

    (n_Nothings == 1) || return false
    (n_Nothings + n_ps == n_inputs) || return false
    (1 in p_positions) || return false

    return true
end

#---------------
# Marginal Rules
#---------------

mutable struct MTransitionMixtureCCCDX <: MarginalRule{TransitionMixture} end
function isApplicable(::Type{MTransitionMixtureCCCDX}, input_types::Vector{<:Type})
    for (i, input_type) in enumerate(input_types)
        if (i == 1)
            matches(input_type, Message{Categorical}) || return false
        elseif (i == 2)
            matches(input_type, Message{Categorical}) || return false
        elseif (i == 3)
            matches(input_type, Message{Categorical}) || return false
        else
            matches(input_type, ProbabilityDistribution) || return false
        end
    end
    return true
end

mutable struct MTransitionMixtureCCCNX <: MarginalRule{TransitionMixture} end
function isApplicable(::Type{MTransitionMixtureCCCNX}, input_types::Vector{<:Type})
    for (i, input_type) in enumerate(input_types)
        if (i == 1)
            matches(input_type, Message{Categorical}) || return false
        elseif (i == 2)
            matches(input_type, Message{Categorical}) || return false
        elseif (i == 3)
            matches(input_type, Message{Categorical}) || return false
        else
            (input_type == Nothing) || return false
        end
    end
    return true
end


export GammaMixture, GammaMixtureNode, GammaMixtureNodeMetadata

# Gamma Mixture Functional Form
struct GammaMixture{N} end

# Special node
# Generic FactorNode implementation does not work with dynamic number of inputs
# We need to reimplement the following set of functions
# functionalform(factornode::FactorNode)
# sdtype(factornode::FactorNode)
# interfaces(factornode::FactorNode)
# factorisation(factornode::FactorNode)
# localmarginals(factornode::FactorNode)
# localmarginalnames(factornode::FactorNode)
# metadata(factornode::FactorNode)
# get_pipeline_stages(factornode::FactorNode)
#
# setmarginal!(factornode::FactorNode, cname::Symbol, marginal)
# getmarginal!(factornode::FactorNode, localmarginal::FactorNodeLocalMarginal)
#
# functional_dependencies(factornode::FactorNode, iindex::Int)
# get_messages_observable(factornode, message_dependencies)
# get_marginals_observable(factornode, marginal_dependencies)
#
# score(::Type{T}, ::FactorBoundFreeEnergy, ::Stochastic, node::AbstractFactorNode, scheduler) where T
#
# Base.show
#
const GammaMixtureNodeFactorisationSupport = Union{MeanField, }

struct GammaMixtureNode{N, F <: GammaMixtureNodeFactorisationSupport, M, P} <: AbstractFactorNode
    factorisation :: F

    # Interfaces
    out    :: NodeInterface
    switch :: NodeInterface
    as  :: NTuple{N, IndexedNodeInterface}
    bs  :: NTuple{N, IndexedNodeInterface}

    meta     :: M
    pipeline :: P
end

functionalform(factornode::GammaMixtureNode{N}) where N = GammaMixture{N}
sdtype(factornode::GammaMixtureNode)                    = Stochastic()
interfaces(factornode::GammaMixtureNode)                = (factornode.out, factornode.switch, factornode.as..., factornode.bs...)
factorisation(factornode::GammaMixtureNode)             = factornode.factorisation
localmarginals(factornode::GammaMixtureNode)            = error("localmarginals() function is not implemented for GammaMixtureNode")
localmarginalnames(factornode::GammaMixtureNode)        = error("localmarginalnames() function is not implemented for GammaMixtureNode")
metadata(factornode::GammaMixtureNode)                  = factornode.meta
getpipeline(factornode::GammaMixtureNode)               = factornode.pipeline

setmarginal!(factornode::GammaMixtureNode, cname::Symbol, marginal)                = error("setmarginal() function is not implemented for GammaMixtureNode")
getmarginal!(factornode::GammaMixtureNode, localmarginal::FactorNodeLocalMarginal) = error("getmarginal() function is not implemented for GammaMixtureNode")

## activate!

struct GammaMixtureNodeFunctionalDependencies <: AbstractNodeFunctionalDependenciesPipeline end

default_functional_dependencies_pipeline(::Type{ <: GammaMixture }) = GammaMixtureNodeFunctionalDependencies()

function functional_dependencies(::GammaMixtureNodeFunctionalDependencies, factornode::GammaMixtureNode{N, F}, iindex::Int) where { N, F <: MeanField }
    message_dependencies = ()

    marginal_dependencies = if iindex === 1
        (factornode.switch, factornode.as, factornode.bs)
    elseif iindex === 2
        (factornode.out, factornode.as, factornode.bs)
    elseif 2 < iindex <= N + 2
        (factornode.out, factornode.switch, factornode.bs[iindex - 2])
    elseif N + 2 < iindex <= 2N + 2
        (factornode.out, factornode.switch, factornode.as[iindex - N - 2])
    else
        error("Invalid index in functional_dependencies for GammaMixtureNode")
    end

    return message_dependencies, marginal_dependencies
end

function get_messages_observable(factornode::GammaMixtureNode{N, F}, message_dependencies::Tuple{}) where { N, F <: MeanField }
    return nothing, of(nothing)
end

function get_marginals_observable(
    factornode::GammaMixtureNode{N, F},
    marginal_dependencies::Tuple{ NodeInterface, NTuple{N, IndexedNodeInterface}, NTuple{N, IndexedNodeInterface} }) where { N, F <: MeanField }

    varinterface = marginal_dependencies[1]
    asinterfaces = marginal_dependencies[2]
    bsinterfaces = marginal_dependencies[3]

    marginal_names = Val{ (name(varinterface), name(asinterfaces[1]), name(bsinterfaces[1])) }
    marginals_observable = combineLatest((
        getmarginal(connectedvar(varinterface), IncludeAll()),
        combineLatest(map((rate) -> getmarginal(connectedvar(rate), IncludeAll()), reverse(bsinterfaces)), PushNew()),
        combineLatest(map((shape) -> getmarginal(connectedvar(shape), IncludeAll()), reverse(asinterfaces)), PushNew()),
    ), PushNew()) |> map_to((
        getmarginal(connectedvar(varinterface), IncludeAll()),
        map((shape) -> getmarginal(connectedvar(shape), IncludeAll()), asinterfaces),
        map((rate) -> getmarginal(connectedvar(rate), IncludeAll()), bsinterfaces)
    ))

    return marginal_names, marginals_observable
end

function get_marginals_observable(
    factornode::GammaMixtureNode{N, F},
    marginal_dependencies::Tuple{ NodeInterface, NodeInterface, IndexedNodeInterface }) where { N, F <: MeanField }

    outinterface    = marginal_dependencies[1]
    switchinterface = marginal_dependencies[2]
    varinterface    = marginal_dependencies[3]

    marginal_names       = Val{ (name(outinterface), name(switchinterface), name(varinterface)) }
    marginals_observable = combineLatestUpdates((
        getmarginal(connectedvar(outinterface), IncludeAll()),
        getmarginal(connectedvar(switchinterface), IncludeAll()),
        getmarginal(connectedvar(varinterface), IncludeAll())
    ), PushNew())

    return marginal_names, marginals_observable
end

# FreeEnergy related functions

@average_energy GammaMixture (q_out::Any, q_switch::Any, q_a::NTuple{N, Any}, q_b::NTuple{N, GammaShapeRate}) where N = begin
    z_bar = probvec(q_switch)
    return mapreduce(+, 1:N, init = 0.0) do i
        return z_bar[i] * score(AverageEnergy(), GammaShapeRate, Val{ (:out, :α , :β) }, map((q) -> Marginal(q, false, false), (q_out, q_a[i], q_b[i])), nothing)
    end
end

function score(::Type{T}, objective::BetheFreeEnergy, ::FactorBoundFreeEnergy, ::Stochastic, node::GammaMixtureNode{N, MeanField}, scheduler) where { T <: InfCountingReal, N }

    skip_strategy = marginal_skip_strategy(objective)

    stream = combineLatest((
        getmarginal(connectedvar(node.out), skip_strategy) |> schedule_on(scheduler),
        getmarginal(connectedvar(node.switch), skip_strategy) |> schedule_on(scheduler),
        combineLatest(map((as) -> getmarginal(connectedvar(as), skip_strategy) |> schedule_on(scheduler), node.as), PushNew()),
        combineLatest(map((bs) -> getmarginal(connectedvar(bs), skip_strategy) |> schedule_on(scheduler), node.bs), PushNew())
    ), PushNew())

    mapping = let fform = functionalform(node), meta = metadata(node)
        (marginals) -> begin
            average_energy   = score(AverageEnergy(), fform, Val{ (:out, :switch, :a, :b) }, marginals, meta)

            out_entropy     = score(DifferentialEntropy(), marginals[1])
            switch_entropy  = score(DifferentialEntropy(), marginals[2])
            a_entropies = mapreduce((m) -> score(DifferentialEntropy(), m), +, marginals[3])
            b_entropies = mapreduce((m) -> score(DifferentialEntropy(), m), +, marginals[4])

            return convert(T, average_energy - (out_entropy + switch_entropy + a_entropies + b_entropies))
        end
    end

    return stream |> map(T, mapping)
end

as_node_functional_form(::Type{ <: GammaMixture }) = ValidNodeFunctionalForm()

# Node creation related functions

sdtype(::Type{ <: GammaMixture }) = Stochastic()

collect_factorisation(::Type{ <: GammaMixture }, factorisation) = factorisation

function ReactiveMP.make_node(::Type{ <: GammaMixture{N} }; factorisation::F = MeanField(), meta::M = nothing, pipeline::P = nothing) where { N, F, M, P }
    @assert N >= 2 "GammaMixtureNode requires at least two mixtures on input"
    @assert typeof(factorisation) <: GammaMixtureNodeFactorisationSupport "GammaMixtureNode supports only following factorisations: [ $(GammaMixtureNodeFactorisationSupport) ]"
    out    = NodeInterface(:out)
    switch = NodeInterface(:switch)
    as   = ntuple((index) -> IndexedNodeInterface(index, NodeInterface(:a)), N)
    bs   = ntuple((index) -> IndexedNodeInterface(index, NodeInterface(:b)), N)
    meta = collect_meta(GammaMixture, meta)
    return GammaMixtureNode{N, F, typeof(meta), P}(factorisation, out, switch, as, bs, meta, pipeline)
end

function ReactiveMP.make_node(::Type{ <: GammaMixture }, out::AbstractVariable, switch::AbstractVariable, as::NTuple{N, AbstractVariable}, bs::NTuple{N, AbstractVariable}; factorisation = MeanField(), meta = nothing, pipeline = nothing) where { N}
    node = make_node(GammaMixture{N}, factorisation = collect_factorisation(GammaMixture, factorisation), meta = collect_meta(GammaMixture, meta), pipeline = collect_pipeline(GammaMixture, pipeline))

    # out
    out_index = getlastindex(out)
    connectvariable!(node.out, out, out_index)
    setmessagein!(out, out_index, messageout(node.out))

    # switch
    switch_index = getlastindex(switch)
    connectvariable!(node.switch, switch, switch_index)
    setmessagein!(switch, switch_index, messageout(node.switch))

    # as
    foreach(zip(node.as, as)) do (ainterface, avar)
        shape_index = getlastindex(avar)
        connectvariable!(ainterface, avar, shape_index)
        setmessagein!(avar, shape_index, messageout(ainterface))
    end

    # bs
    foreach(zip(node.bs, bs)) do (binterface, bvar)
        rate_index = getlastindex(bvar)
        connectvariable!(binterface, bvar, rate_index)
        setmessagein!(bvar, rate_index, messageout(binterface))
    end

    return node
end

function ReactiveMP.make_node(fform::Type{ <: GammaMixture }, autovar::AutoVar, args::Vararg{ <: AbstractVariable }; kwargs...)
    var  = randomvar(getname(autovar))
    node = make_node(fform, var, args...; kwargs...)
    return node, var
end


using UnicodeGraphics, OffsetArrays, Test

pac = [0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0;
       0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0;
       0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0;
       0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0;
       0 0 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 1 0;
       0 1 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 1 0;
       0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0;
       1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0;
       1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0;
       1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0;
       1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1;
       1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0;
       1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0;
       0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0;
       0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0;
       0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0;
       0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0;
       0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0;
       0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0;
       0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0]

block_pac = """
     ▄▄██████▄▄
  ▄██████████████▄
 ▄███████  ████████
▄██████████████▀▀
███████████▀▀
███████████▄▄      ▀
▀██████████████▄▄
 ▀█████████████████
  ▀██████████████▀
     ▀▀██████▀▀     \0"""

test_pac = blockize(pac)
@test test_pac == block_pac
println(test_pac, "\n\n", block_pac, "\n\n")

brail_pac =
"""
⠀⣠⣴⣾⣿⣿⣷⣦⣄
⣰⣿⣿⣿⣧⣼⣿⡿⠟⠃
⣿⣿⣿⣿⣿⣏⡁⠀⠀⠠
⠹⣿⣿⣿⣿⣿⣿⣷⣦⡄
⠀⠙⠻⢿⣿⣿⡿⠟⠋⠀\0"""

test_pac = brailize(pac)
@test test_pac == brail_pac
println(test_pac, "\n\n", brail_pac, "\n\n")

ghost = [1.0 0.0 1.0 0.0 1.0 0.0 1.0 0.0 1.0 0.0 1.0 0.0 1.0 0.0 1.0 0.0 1.0 0.0 1.0;
         0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0;
         1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 1.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0;
         0.0 0.0 0.0 0.0 0.0 0.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0;
         1.0 0.0 0.0 0.0 0.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 0.0 0.0 0.0 1.0;
         0.0 0.0 0.0 0.0 1.0 0.0 0.0 1.0 1.0 1.0 1.0 0.0 0.0 1.0 1.0 1.0 0.0 0.0 0.0;
         1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 1.0 0.0 0.0 0.0 0.0 1.0 1.0 0.0 0.0 1.0;
         0.0 0.0 0.0 0.0 1.0 1.0 0.0 0.0 1.0 1.0 1.0 1.0 0.0 0.0 1.0 1.0 0.0 0.0 0.0;
         1.0 0.0 0.0 1.0 1.0 1.0 0.0 0.0 1.0 1.0 1.0 1.0 0.0 0.0 1.0 1.0 1.0 0.0 1.0;
         0.0 0.0 0.0 1.0 1.0 0.0 0.0 1.0 1.0 1.0 1.0 0.0 0.0 1.0 1.0 1.0 1.0 0.0 0.0;
         1.0 0.0 0.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 0.0 1.0;
         0.0 0.0 0.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 0.0 0.0;
         1.0 0.0 0.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 0.0 1.0;
         0.0 0.0 0.0 1.0 1.0 0.0 1.0 1.0 1.0 0.0 0.0 1.0 1.0 1.0 0.0 1.0 1.0 0.0 0.0;
         1.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 1.0 0.0 0.0 1.0 1.0 0.0 0.0 0.0 1.0 0.0 1.0;
         0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0;
         1.0 0.0 1.0 0.0 1.0 0.0 1.0 0.0 1.0 0.0 1.0 0.0 1.0 0.0 1.0 0.0 1.0 0.0 1.0]

block_ghost =
"""
   ▄▄████▄▄
 ▄▀▀████▀▀██▄
 ▄▄  ██▄▄  ██
██▀ ▄███▀ ▄███
██████████████
██▀███▀▀███▀██
▀   ▀▀  ▀▀   ▀\0"""

test_ghost = blockize(OffsetArray(ghost[3:15, 4:17], 3:15, 4:17), 0.5)
@test test_ghost == block_ghost
println(test_ghost, "\n\n", block_ghost, "\n\n")

braile_ghost =
"""
⠀⣠⣴⣶⣦⣄
⣨⡄⢹⣯⡄⢹⣇
⣿⣶⣿⣿⣶⣿⣿
⠋⠈⠛⠀⠛⠁⠙\0"""

test_ghost = brailize(OffsetArray(ghost[2:15, 4:17], 2:15, 4:17), 0.5)
@test test_ghost == braile_ghost
println(test_ghost, "\n\n", braile_ghost)


function PolygonalFieldOfView(arg0::SphericalPolygonsSet, arg1::jdouble)
    return PolygonalFieldOfView((SphericalPolygonsSet, jdouble), arg0, arg1)
end

function PolygonalFieldOfView(arg0::Vector3D, arg1::PolygonalFieldOfView_DefiningConeType, arg2::Vector3D, arg3::jdouble, arg4::jint, arg5::jdouble)
    return PolygonalFieldOfView((Vector3D, PolygonalFieldOfView_DefiningConeType, Vector3D, jdouble, jint, jdouble), arg0, arg1, arg2, arg3, arg4, arg5)
end


# this file defines iterators used for looping over a grid
struct UpdateFlags
    nodes::Bool
    coords::Bool
    celldofs::Bool
end

UpdateFlags(; nodes::Bool=true, coords::Bool=true, celldofs::Bool=true) =
    UpdateFlags(nodes, coords, celldofs)

"""
    CellIterator(grid::Grid)
    CellIterator(grid::DofHandler)

Return a `CellIterator` to conveniently loop over all the cells in a grid.

# Examples
```julia
for cell in CellIterator(grid)
    coords = getcoordinates(cell) # get the coordinates
    dofs = celldofs(cell)         # get the dofs for this cell
    reinit!(cv, cell)             # reinit! the FE-base with a CellIterator
end
```
"""
struct CellIterator{dim,N,T,M}
    flags::UpdateFlags
    grid::Grid{dim,N,T,M}
    current_cellid::ScalarWrapper{Int}
    nodes::Vector{Int}
    coords::Vector{Vec{dim,T}}
    dh::DofHandler{dim,N,T,M}
    celldofs::Vector{Int}

    function CellIterator{dim,N,T,M}(dh::DofHandler{dim,N,T,M}, flags::UpdateFlags) where {dim,N,T,M}
        cell = ScalarWrapper(0)
        nodes = zeros(Int, N)
        coords = zeros(Vec{dim,T}, N)
        n = ndofs_per_cell(dh)
        celldofs = zeros(Int, n)
        return new{dim,N,T,M}(flags, dh.grid, cell, nodes, coords, dh, celldofs)
    end

    function CellIterator{dim,N,T,M}(grid::Grid{dim,N,T,M}, flags::UpdateFlags) where {dim,N,T,M}
        cell = ScalarWrapper(0)
        nodes = zeros(Int, N)
        coords = zeros(Vec{dim,T}, N)
        return new{dim,N,T,M}(flags, grid, cell, nodes, coords)
    end
end

CellIterator(grid::Grid{dim,N,T,M},     flags::UpdateFlags=UpdateFlags()) where {dim,N,T,M} =
    CellIterator{dim,N,T,M}(grid, flags)
CellIterator(dh::DofHandler{dim,N,T,M}, flags::UpdateFlags=UpdateFlags()) where {dim,N,T,M} =
    CellIterator{dim,N,T,M}(dh, flags)

# iterator interface
function Base.iterate(ci::CellIterator, state = 1)
    if state > getncells(ci.grid)
        return nothing
    else
        return (reinit!(ci, state), state+1)
    end
end
Base.length(ci::CellIterator)  = getncells(ci.grid)

Base.IteratorSize(::Type{T})   where {T<:CellIterator} = Base.HasLength() # this is default in Base
Base.IteratorEltype(::Type{T}) where {T<:CellIterator} = Base.HasEltype() # this is default in Base
Base.eltype(::Type{T})         where {T<:CellIterator} = T

# utility
@inline getnodes(ci::CellIterator) = ci.nodes
@inline getcoordinates(ci::CellIterator) = ci.coords
@inline nfaces(ci::CellIterator) = nfaces(eltype(ci.grid.cells))
@inline onboundary(ci::CellIterator, face::Int) = ci.grid.boundary_matrix[face, ci.current_cellid[]]
@inline cellid(ci::CellIterator) = ci.current_cellid[]
@inline celldofs!(v::Vector, ci::CellIterator) = celldofs!(v, ci.dh, ci.current_cellid[])
@inline celldofs(ci::CellIterator) = ci.celldofs

function reinit!(ci::CellIterator{dim,N}, i::Int) where {dim,N}
    nodeids = ci.grid.cells[i].nodes
    ci.current_cellid[] = i
    @inbounds for j in 1:N
        nodeid = nodeids[j]
        ci.flags.nodes  && (ci.nodes[j] = nodeid)
        ci.flags.coords && (ci.coords[j] = ci.grid.nodes[nodeid].x)
    end
    if isdefined(ci, :dh) && ci.flags.celldofs # update celldofs
        celldofs!(ci.celldofs, ci)
    end
    return ci
end

@inline reinit!(cv::CellValues{dim,T}, ci::CellIterator{dim,N,T}) where {dim,N,T} = reinit!(cv, ci.coords)
@inline reinit!(fv::FaceValues{dim,T}, ci::CellIterator{dim,N,T}, face::Int) where {dim,N,T} = reinit!(fv, ci.coords, face)


"""
This module provides utilities to build neural networks with Knet,
along with a library of standard architectures.
"""
module KNets

export SimpleNet, SimpleNetHP, ResNet, ResNetHP

using ..Network
using Base: @kwdef
import ..GameInterface, ..Util, ..CyclicSchedule

import Knet

include("knet/layers.jl")

#####
##### Traversal utilities
#####

children(x) = ()

mapchildren(f, x) = Util.@unimplemented

function traverse!(f, model, seen=IdDict())
  haskey(seen, model) && return
  seen[model] = true
  f(model)
  for c in children(model)
    traverse!(f, c, seen)
  end
end

function gather(f, model)
  acc = Any[]
  traverse!(x -> append!(acc, f(x)), model)
  return acc
end

function fmap(f, model, cache=IdDict())
  haskey(cache, model) && (return cache[model])
  return cache[model] =
    isempty(children(model)) ?
      f(model) :
      f(mapchildren(c -> fmap(f, c, cache), model))
end

#####
##### Implementing the `Network` interface
#####

"""
    KNetwork{Game} <: AbstractNetwork{Game}

Abstract type for neural networks implemented using the _Knet_ framework.

  - Subtypes are expected to be expressed as the composition of Flux-like
    layers that implement a functor interface through functions `children` and
    `mapchildren`.
  - A custom implementation of `regularized_params_` must also
    be implemented for layers containing parameters that are subject to
    regularization.

Provided that the above holds, `KNetwork` implements the full network interface
with the following exceptions:
[`Network.HyperParams`](@ref), [`Network.hyperparams`](@ref),
[`Network.forward`](@ref) and [`Network.on_gpu`](@ref).
"""
abstract type KNetwork{Game} <: AbstractNetwork{Game} end

Base.copy(nn::KNetwork) = Base.deepcopy(nn)

const GPU_AVAILABLE = Knet.gpu() >= 0

Network.to_gpu(nn::KNetwork) = GPU_AVAILABLE ? Knet.gpucopy(nn) : nn
Network.to_cpu(nn::KNetwork) = GPU_AVAILABLE ? Knet.cpucopy(nn) : nn

params_(x) = []
params_(x::Knet.Param) = [x]
Network.params(nn::KNetwork) = gather(params_, nn)

regularized_params_(x) = []
regularized_params_(m::Dense) = [m.W]
regularized_params_(m::Conv) = [m.W]
Network.regularized_params(nn::KNetwork) = gather(regularized_params_, nn)

set_test_mode!_(x, mode) = []
set_test_mode!_(l::BatchNorm, mode) = l.train = !mode

Network.set_test_mode!(nn::KNetwork, mode) =
  traverse!(x -> set_test_mode!_(x, mode), nn)

Network.convert_input(nn::KNetwork, x) =
  Network.on_gpu(nn) ? Knet.KnetArray(x) : Array(x)

Network.convert_output(::KNetwork, x) = x
Network.convert_output(::KNetwork, x::Knet.KnetArray) = Array(x)

function Network.train!(
    callback, nn::KNetwork, opt::CyclicNesterov, loss, data, n)
  lr = CyclicSchedule(
    opt.lr_base,
    opt.lr_high,
    opt.lr_low, n=n)
  momentum = CyclicSchedule(
    opt.momentum_high,
    opt.momentum_low,
    opt.momentum_high, n=n)
  optimiser = Knet.Nesterov(lr=opt.lr_low, gamma=opt.momentum_high)
  for (i, l) in enumerate(Knet.minimize(loss, data, optimiser))
    callback(i, l)
    optimiser.lr = lr[i]
    optimiser.gamma = momentum[i]
  end
end

function Network.train!(callback, nn::KNetwork, opt::Adam, loss, data, n)
  optimiser = Knet.Adam(lr=opt.lr)
  for (i, l) in enumerate(Knet.minimize(loss, data, optimiser))
    callback(i, l)
  end
end

function Network.gc(::KNetwork)
  GPU_AVAILABLE || return
  GC.gc(true)
  Knet.gc()
end

#####
##### Common functions between two-head neural networks
#####

"""
    TwoHeadNetwork{Game} <: KNetwork{G}

An abstract type for two-head neural networks implemented with Knet.

Subtypes are assumed to have the following fields:
`hyper`, `common`, `vhead` and `phead`. Based on those, an implementation
is provided for [`Network.hyperparams`](@ref), [`Network.forward`](@ref) and
[`Network.on_gpu`](@ref), leaving only [`Network.HyperParams`](@ref) to
be implemented.
"""
abstract type TwoHeadNetwork{G} <: KNetwork{G} end

function Network.forward(nn::TwoHeadNetwork, board)
  c = nn.common(board)
  v = nn.vhead(c)
  p = nn.phead(c)
  return (p, v)
end

children(nn::TwoHeadNetwork) = (nn.common, nn.vhead, nn.phead)

function mapchildren(f, nn::Net) where Net <: TwoHeadNetwork
  Net(nn.hyper, f(nn.common), f(nn.vhead), f(nn.phead))
end

Network.hyperparams(nn::TwoHeadNetwork) = nn.hyper

function Network.on_gpu(nn::TwoHeadNetwork)
  b = nn.vhead.layers[end].b
  return isa(Knet.value(b), Knet.KnetArray)
end

#####
##### Include networks library
#####

include("architectures/simplenet.jl")
include("architectures/resnet.jl")

end


# Functions to implement goal functions and testing for goal functions
using Printf
export Goal, GoalList, randgoal, randgoallist, rand_env_goallist, env_goal, goal_count, goal_check, my_test_goal
export randgoal_filtered, randgoallist_filtered

# Moved these to aliases.jl
#const Goal =  Vector{MyInt}
#const GoalList = Vector{Goal}

#=
goal1 = MyInt[ 0xb, 0xe ]
goal2 = MyInt[ 0x6, 0x9 ]
goal3 = MyInt[ 0x1, 0xe ]
goal4 = MyInt[ 0x1, 0xe ]
goal5 = MyInt[ 0x3, 0x9 ]
goallist2 = [goal1, goal2, goal3 ]

goallist3 = [0x50, 0x05, 0xf5, 0xa0, 0xe0, 0x0e, 0xf1, 0x80, 0x08, 0xfa]
=#

function component_type( numinputs::Int64 )
  if numinputs <= 2
    comp_type = collect(0x00:0x0f)
  elseif numinputs == 3
    comp_type = collect(0x00:0xff)
  elseif numinputs == 4
    comp_type = UInt16
  elseif numinputs == 5
    comp_type = UInt32
  elseif numinputs == 6
    comp_type = UInt64
  elseif numinputs == 7
    comp_type = UInt128
  elseif numinputs > 7
    error("randgoal doesn't work for numinputs > 7")
  end
  comp_type
end

# generate a random goal assuming that MyInt is set correctly for numinputs
# Creates an inexacterror if conversion to MyInt doesn't work
function randgoal(numinputs::Int64, numoutputs::Int64 )
  map(x->convert( MyInt, x), rand( component_type( numinputs ), numoutputs ) )
end

function randgoal(p::Parameters)
  randgoal(p.numinputs,p.numoutputs)
end

# generate a random goallist of length ngoals
function randgoallist(ngoals::Int64, numinputs::Int64, numoutputs::Int64; repetitions::Int64=1)
  result = Vector{MyInt}[]
  if ngoals % repetitions != 0
    error("ngoals=",ngoals," must be a multiple of repetitions=",repetitions," in randgoallist")
  end
  #println("  collect(1:repetitions.ngoals): ",collect(1:repetitions:ngoals))
  for i = 1:repetitions:ngoals
    goal = rand( component_type(numinputs), numoutputs )
    for j = 1:repetitions
      #println("(i,j): ",(i,j)," i+j-1: ",i+j-1)
      #result[i+j-1] = goal
      push!(result,goal)
    end
  end
  result
end

function randgoallist(ngoals::Int64, p::Parameters; repetitions::Int64=1)
  randgoallist( ngoals, p.numinputs, p.numoutputs, repetitions=repetitions )
end

# Returns a goal list of ngoals which is obtained by generating div(ngoals,repetitions)
#   goals, and then perturing these base goals repetitions-1 times to generate
#   the additional goals.
# If use_env_goals==true, then repeat goals are obtained by flipping specific bits
#    of the base goals
# If use_env_goals==false, then repeat goals are obtained by flipping random bits
#   the additional goals.
function rand_env_goallist( ngoals::Int64, numinputs::Int64, numoutputs::Int64,
      repetitions::Int64, num_flipped_bits::Int64; perturb_goal::Bool=true )
  #println("env goallist: perturb_goal: ",perturb_goal)
  result = Vector{MyInt}[]
  if numoutputs % repetitions != 0
    error("numoutputs=",numoutputs," must be a multiple of repetitions=",repetitions," in rand_env_goallist")
  end
  #println("f_bit_list: ",f_bits_list)
  for i = 1:ngoals
    #g = rand( component_type(numinputs), div( numoutputs, repetitions ) )
    g = randgoal( numinputs, div(numoutputs,repetitions) )
    if perturb_goal
      goal = perturb_bits_goal( g, num_flipped_bits, repetitions, numinputs )
    else
      f_bits_list = flipped_bits_list( num_flipped_bits, repetitions-1, numinputs )
      goal = env_goal( g, f_bits_list )
    end
    Base.push!(result,goal)
  end
  result
end

# return a random goal with a minimum count of min_count in the dataframe column cdf[count_field].
# The goal is filtered by count from the dataframe column cdf[count_field].
function randgoal_filtered( cdf::DataFrame, count_field::Symbol, min_count::Int64, numinputs::Int64, numoutputs::Int64 )
  max_iters = 20    # prevent an infinite loop
  goal = randgoal( numinputs, numoutputs )
  iter = 0
  count = cdf[cdf.goal.==@sprintf("0x%x",goal[1]),count_field][1]
  while iter < max_iters && count < min_count
    goal = randgoal( numinputs, numoutputs )
    println("goal: ",goal)
    count_array = cdf[cdf.goal.==@sprintf("0x%x",goal[1]),count_field]
    println("count_array: ",count_array)
    count = count_array[1]
    iter += 1
  end
  if iter < max_iters
    goal
  else
    error("too many iterations in randgoal filtered")
  end
end

# generate a random goallist of length ngoals
# The goals are filtered by count from the dataframe cdf field count_field.
function randgoallist_filtered( cdf::DataFrame, count_field::Symbol, min_count::Int64, ngoals::Int64, numinputs::Int64,
     numoutputs::Int64; repetitions::Int64=1)
  result = Vector{MyInt}[]
  if ngoals % repetitions != 0
    error("ngoals=",ngoals," must be a multiple of repetitions=",repetitions," in randgoallist")
  end
  #println("  collect(1:repetitions.ngoals): ",collect(1:repetitions:ngoals))
  for i = 1:repetitions:ngoals
    goal = randgoal_filtered( cdf, count_field, min_count, numinputs, numoutputs )
    for j = 1:repetitions
      #println("(i,j): ",(i,j)," i+j-1: ",i+j-1)
      #result[i+j-1] = goal
      push!(result,goal)
    end
  end
  result
end

# The bits that will be flipped to produce the additional goals in function env_goal()
# Example (where MyInt==UInt16):
# julia> flipped_bits_list( 2,3,3)
# 3-element Array{UInt16,1}:
#  0x00c0
#  0x0030
#  0x000c
function flipped_bits_list( num_flipped_bits::Int64, repetitions::Int64, numinputs::Int64 )
  one = MyInt(1)
  if num_flipped_bits > 0
    ones = one
    for i = 1:(num_flipped_bits-1)
      ones = ones<<1 | one
    end
  else
    ones = MyInt(0)   # No flips if num_flipped_bit == 0
  end
  fb = ones << (2^numinputs-num_flipped_bits)
  #Printf.@printf("flipped_bits_list: ones:  0x%04x  fb: 0x%04x\n",ones,fb)
  [ fb >> (num_flipped_bits*i) for i = 0:(repetitions-1) ]
end

# returns a new goal with each component of goal replicated length(flipped_bits_list) times,
#   where each replication has nbits_to_perturb bits of the original component perturbed
function env_goal( goal::Goal, flipped_bits_list::Vector{MyInt} )
  #println("env goal")
  repetitions = length(flipped_bits_list)
  new_goal = Goal()
  for c in goal
    Base.push!( new_goal, c )
    for i = 1:repetitions
      Base.push!( new_goal, xor( c, flipped_bits_list[i] ))
    end
  end
  #println("new_goal: ",new_goal)
  new_goal
end

# returns an new goal with each component of goal replicated repetitions times,
#   where each replication has nbits_to_perturb randomly chosen bits
#   of the original component perturbed
function perturb_bits_goal( goal::Goal, nbits_to_perturb::Int64, repetitions::Int64, numinputs::Int64 )
  #println("perturb goal")
  new_goal = Goal()
  for c in goal
    push!( new_goal, c )
    for i = 1:repetitions-1
      rp = rand_perturbation( 2^numinputs, nbits_to_perturb )
      #Printf.@printf("rp: 0x%04x\n",rp)
      #push!( new_goal, xor( c, rand_perturbation( 2^numinputs, nbits_to_perturb )))
      push!( new_goal, xor( c, rp))
   end
  end
  #println("ptb: Goal: ",goal)
  new_goal
end

# Returns a MyInt with nnbits_to_perturb 1 bits.
# When perturbing a goal, maxlen should be 2^numinputs
function rand_perturbation( maxlength::Int64, nbits_to_perturb::Int64 )
  bits_to_perturb = rand_set( maxlength, nbits_to_perturb )
  #println("bits_to_perturb: ",bits_to_perturb)
  result = MyInt(0)
  for i in bits_to_perturb
    result |= MyInt(1) << i
  end
  result
end

# returns a random set of n_elements intgers from the integer range [0,maxlen-1]
function rand_set( maxlen::Int64, n_elements::Int64 )
  @assert n_elements < maxlen
  S = Int64[]
  while length(unique!(S)) < n_elements
    push!(S,rand(0:(maxlen-1)))
  end
  S
end

# Returns true if testgoal is equal to any of the goals in goallist defined above
function goal_check( testgoal::Goal, goallist::GoalList )
  length( filter( x -> testgoal==x, goallist )) > 0
end

# Returns the number of components of testGoal that are equal to the corresponding component of goal.
# Never used
#function my_test_goal( testGoal::Goal, goal::Goal )
#  length( filter( x -> x == 0, testGoal .⊻ goal ))
#end

function my_test_goal( testGoal::Goal, goallist::GoalList )
  achieved = 0
  for g in goallist
    ga = length( filter( x -> x == 0, testGoal .⊻ g ))
    if ga > achieved
      achieved = ga
    end
    println("my_test_goal: achieved: ",achieved)
  end
  achieved
end

# Returns a 2-tuple of:
#    1.  the  maximum number of matching subgoals of testgoal to components of goallist
#    2.  the  index of the goal of goallist that gave this maximum
# If testgoal equals all subgoals of a goal of goallist, then returns numouputs and the index of the goal in goallist
# If no component of testgoal matches the corresponding component of any goal in goallist, return 0
function goal_count( testgoal::Goal, goallist::GoalList )
   findmax( map( x -> my_test_goal( x, testgoal ), goallist ))[1]
end

# Test if at least numsubgoals components of testgoal match a component of goal (in any order)
#=
function my_test_goal( testgoal::Goal, goal::Goal, numsubgoals::Integer )
  filter( x -> (Base.findfirst( y -> y == x, testgoal ) != nothing ), goal)
end
=#


using NestedEnvironments
using DifferentialEquations
using Test

## user-defined nested environments
struct SubSubEnv <: AbstractEnv
end
struct SubEnv <: AbstractEnv
    env21::SubSubEnv
    env22::SubSubEnv
end
struct Env <: AbstractEnv
    env1::SubSubEnv
    env2::SubEnv
end
# dynamics
function f(x, p, t)
    ẋ1 = -x.env1
    ẋ21 = -x.env2.env21
    ẋ22 = -x.env2.env22
    (; env1 = ẋ1, env2 = (; env21 = ẋ21, env22 = ẋ22))
end
# initial condition
function initial_condition(env::Env)
    (; env1 = 1, env2 = (; env21 = rand(4), env22 = rand(2, 7)))
end

# for register
__env = Env(SubSubEnv(), SubEnv(SubSubEnv(), SubSubEnv()))
__x0 = initial_condition(__env)
@reg_env __env __x0

## test
function test1()
    env = Env(SubSubEnv(), SubEnv(SubSubEnv(), SubSubEnv()))
    x0 = initial_condition(env)
    @test x0 == @readable @raw x0
end

function test2()
    env = Env(SubSubEnv(), SubEnv(SubSubEnv(), SubSubEnv()))
    x0 = initial_condition(env)
    _x0 = @raw env x0
    x0_new = @readable env _x0
    @test x0 == x0_new
end

function test3()
    env = Env(SubSubEnv(), SubEnv(SubSubEnv(), SubSubEnv()))
    x0 = initial_condition(env)
    t0 = 0.0
    tf = 100.0
    tspan = (t0, tf)
    ts = t0:0.01:tf
    prob = ODEProblem(env, f, x0, tspan)
    sol = solve(prob, Tsit5(), saveat=ts)
    @test sol.u[1] == @raw env x0
end

function test4()
    env1 = BaseEnv()
    @test NestedEnvironments.initial_condition(env1) == 0.0
    size_tuple = (1, 2)
    env2 = BaseEnv(size_tuple)
    @test NestedEnvironments.initial_condition(env2) == zeros(size_tuple)
    env3 = BaseEnv(size_tuple...)
    @test NestedEnvironments.initial_condition(env3) == zeros(size_tuple)
    x0 = reshape(collect(1:15), 3, 5)
    env4 = BaseEnv(x0)
    @test NestedEnvironments.initial_condition(env4) == x0
end

function test()
    test1()
    test2()
    test3()
    test4()
end


using Mamba

include("utils.jl")

const openbugstests = [
  "asthma",
  "birats",
  "blocker",
  "bones",
  "dogs",
  "dyes",
  "epil",
  "equiv",
  "eyes",
  "inhalers",
  "jaws",
  "leuk",
  "lsat",
  "magnesium",
  "mice",
  "oxford",
  "pumps",
  "rats",
  "salm",
  "seeds",
  "stacks",
  "surgical"
]

const contributedtests = [
  "gk",
  "line_abc",
  "line_amwg_slice",
  "pollution"
]

println("Running examples:")

for t in openbugstests
  @everywhere srand(123)
  @runtest "../doc/examples/" t
  gelmandiag(sim) |> show
end

for t in contributedtests
  @everywhere srand(123)
  @runtest "../doc/examples/" t
end


module ReferenceFrameRotations

import Base: +, -, *, /, \, ≈, ==
import Base: conj, convert, copy, display, eltype, firstindex, getindex, imag
import Base: inv, iterate, lastindex, ndims, one, real, setindex!, show
import Base: zero, zeros
import Base: Broadcast.broadcastable
import LinearAlgebra: norm

using Crayons
using LinearAlgebra
using Printf
using StaticArrays

# Re-export `I` from LinearAlgebra.
export I

################################################################################
#                                    Types
################################################################################

include("types.jl")

################################################################################
#                                  Constants
################################################################################

# Pre-defined crayons.
const _reset_crayon = Crayon(reset = true)
const _crayon_bold  = crayon"bold"
const _crayon_g     = crayon"bold green"
const _crayon_u     = crayon"bold blue"
const _crayon_y     = crayon"bold yellow"

# Escape sequences related to the crayons.
const _b = _crayon_bold
const _d = _reset_crayon
const _g = _crayon_g
const _y = _crayon_y
const _u = _crayon_u

################################################################################
#                                   Includes
################################################################################

include("angle.jl")
include("angleaxis.jl")
include("compose_rotations.jl")
include("dcm.jl")
include("inv_rotations.jl")
include("quaternion.jl")

include("deprecations.jl")

include("./conversions/angle_to_angle.jl")
include("./conversions/angle_to_angleaxis.jl")
include("./conversions/angle_to_dcm.jl")
include("./conversions/angle_to_quat.jl")
include("./conversions/angle_to_rot.jl")
include("./conversions/angleaxis_to_angle.jl")
include("./conversions/angleaxis_to_dcm.jl")
include("./conversions/angleaxis_to_quat.jl")
include("./conversions/dcm_to_angle.jl")
include("./conversions/dcm_to_angleaxis.jl")
include("./conversions/dcm_to_quat.jl")
include("./conversions/quat_to_angle.jl")
include("./conversions/quat_to_angleaxis.jl")
include("./conversions/quat_to_dcm.jl")
include("./conversions/smallangle_to_dcm.jl")
include("./conversions/smallangle_to_quat.jl")
include("./conversions/smallangle_to_rot.jl")

end # module


# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/NodeNumbering.jl/blob/master/LICENSE

module NodeNumbering

include("calc_bw.jl")
include("create_adjacency_graph.jl")
include("node_degrees.jl")
include("RCM.jl")
include("renumbering.jl")
include("create_RCM_adjacency.jl")
include("adjacency_visualization.jl")

end


using MLStyle
using Random: AbstractRNG
using KeywordCalls
using ConstructionBase
export @parameterized, @half

# A fold over ASTs. Example usage in `replace`
function foldast(leaf, branch; kwargs...)
    function go(ast)
        MLStyle.@match ast begin
            Expr(head, args...) => branch(head, map(go, args); kwargs...)
            x                   => leaf(x; kwargs...)
        end
    end

    return go
end

function replace(p, f, expr)
    leaf(s) = p(s) ? f(s) : s
    branch(head, newargs) = Expr(head, newargs...)
    foldast(leaf, branch)(expr)
end

# from https://thautwarm.github.io/MLStyle.jl/latest/tutorials/capture.html
function capture(template, ex, action)
    let template = Expr(:quote, template)
        quote
            @match $ex begin
                $template => $action
                _         => nothing
            end
        end
    end
end

macro capture(template, ex, action)
    capture(template, ex, action) |> esc
end

function _parameterized(__module__, expr)
    @capture ($op($μ($(p...)), $base)) expr begin
        @assert op ∈ [:<<, :≪, :≃]
        μbase = Symbol(:__, μ, :_base)

        μ = esc(μ)
        base = esc(base)

        # @gensym basename
        q = quote
            struct $μ{N,T} <: MeasureBase.ParameterizedMeasure{N}
                par::NamedTuple{N,T}
            end

            const $μbase = $base
            MeasureBase.basemeasure(::$μ) = $μbase
        end

        if !isempty(p)
            # e.g. Normal(μ,σ) = Normal((μ=μ, σ=σ))
            pnames = QuoteNode.(p)
            push!(q.args, :($μ($(p...)) = $μ(NamedTuple{($(pnames...),)}(($(p...),)))))
        end

        return q
    end

    @capture $μ($(p...)) expr begin
        μ = esc(μ)

        q = quote
            struct $μ{N,T} <: MeasureBase.ParameterizedMeasure{N}
                par::NamedTuple{N,T}
            end
        end

        if !isempty(p)
            # e.g. Normal(μ,σ) = Normal((μ=μ, σ=σ))
            pnames = QuoteNode.(p)
            push!(q.args, :($μ($(p...)) = $μ(NamedTuple{($(pnames...),)}(($(p...),)))))
        end

        return q
    end

    @capture $μ($(p...)) expr begin
        μ = esc(μ)

        q = quote
            struct $μ{N,T} <: MeasureBase.ParameterizedMeasure{N}
                par::NamedTuple{N,T}
            end
        end

        if !isempty(p)
            # e.g. Normal(μ,σ) = Normal((μ=μ, σ=σ))
            pnames = QuoteNode.(p)
            push!(q.args, :($μ($(p...)) = $μ(NamedTuple{($(pnames...),)}(($(p...),)))))
        end

        return q
    end
end

"""
    @parameterized <declaration>

The <declaration> gives a measure and its default parameters, and specifies
its relation to its base measure. For example,

    @parameterized Normal(μ,σ)

declares the `Normal` is a measure with default parameters `μ and σ`. The result is equivalent to
```
struct Normal{N,T} <: ParameterizedMeasure{N}
    par :: NamedTuple{N,T}
end

KeywordCalls.@kwstruct Normal(μ,σ)

Normal(μ,σ) = Normal((μ=μ, σ=σ))
```

See [KeywordCalls.jl](https://github.com/cscherrer/KeywordCalls.jl) for details on `@kwstruct`.
"""
macro parameterized(expr)
    _parameterized(__module__, expr)
end

"""
    @half dist([paramnames])

Starting from a symmetric univariate measure `dist ≪ Lebesgue(ℝ)`, create a new
measure `Halfdist ≪ Lebesgue(ℝ₊)`. For example,

    @half Normal()

creates `HalfNormal()`, and

    @half StudentT(ν)

creates `HalfStudentT(ν)`.
"""
macro half(ex)
    _half(__module__, ex)
end

function _half(__module__, ex)
    @match ex begin
        :($dist) => begin
            halfdist = esc(Symbol(:Half, dist))
            dist = esc(dist)
            quote
                $halfdist(args...) = half($dist(args...))
                $halfdist(; kwargs...) = half($dist(; kwargs...))
            end
        end
    end
end


using DataDeps

register(DataDep(
  "auto-mpg",
  "https://archive.ics.uci.edu/ml/datasets/Auto+MPG",
  "https://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/auto-mpg.data",
  "48b830e11feee5572525f8f1691ddb9d38d3d7b7063edcd8fca672c2a5e17d8d",
  post_fetch_method=(path -> begin
    # Inconsistent use of tabs and whitespace delimiting
    # Convert to whitespace
    write(path, replace(read(path, String), '\t' => ' '))

    UCIData.process_dataset(path,
        target_index=1,
        feature_indices=2:8,
        id_indices=9,
        categoric_indices=8,
        delim=' ',
        ignorerepeated=true,
    )
  end),
))


# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

module GmmTests

dir = @__DIR__
gmm_test_implementations = Tuple{String, Float64}[
    ("$dir/../../../../src/julia/modules/Zygote/ZygoteGMM.jl", 1e-8)
]

map!(tup -> (abspath(tup[1]), tup[2]), gmm_test_implementations, gmm_test_implementations)

using Test
include("../../../../src/julia/shared/load.jl")
include("../../../../src/julia/runner/load.jl")
include("load.jl")
import ADPerfTest
import TestLoader
using GMMData
using TestUtils

@testset "GMM Module Tests" begin

@testset "GMM Module Test ($(basename(module_path)))" for (module_path, tolerance) in gmm_test_implementations
    module_dir, module_filename = splitdir(module_path)
    module_name, module_ext = splitext(module_filename)
    # Assert that the path to the module is correct
    ext_r = @test module_ext == ".jl"
    ispath_r = @test (ispath(module_path) && !isdir(module_path))
    if isa(ext_r, Test.Pass) && isa(ispath_r, Test.Pass)
        need_modify_load_path = !(module_dir ∈ LOAD_PATH)
        if need_modify_load_path
            push!(LOAD_PATH, module_dir)
        end
        try
            test = TestLoader.get_gmm_test(module_name)
            # Module loads
            @test isa(test, ADPerfTest.Test{GMMInput,GMMOutput})
            input = load_gmm_input("$dir/../../../../data/gmm/test.txt", false)
            output = empty_gmm_output()
            test.prepare!(test.context, input)
            test.calculate_objective!(test.context, 1)
            test.output!(output, test.context)
            # Objective is calculated correctly
            @test 8.07380408004975791e+00 ≈ output.objective atol=tolerance
            test.calculate_objective!(test.context, 3)
            test.output!(output, test.context)
            # Objective is calculated correctly for times = 3
            @test 8.07380408004975791e+00 ≈ output.objective atol=tolerance
            test.calculate_jacobian!(test.context, 1)
            test.output!(output, test.context)
            # Gradient is calculated correctly
            @test 18 == size(output.gradient, 1)
            correct_grad = [
                1.08662855508652456e-01,
                -7.41270039523898472e-01,
                6.32607184015246071e-01,
                1.11692576532787013e+00,
                1.63333013551455269e-01,
                -2.19989824071193142e-02,
                2.27778292254236098e-01,
                1.20963025612832187e+00,
                -6.06375920733956339e-02,
                2.58529994051162237e+00,
                1.12632694524213789e-01,
                3.85744309849611777e-01,
                7.35180573182305508e-02,
                5.41836362715595232e+00,
                -3.21494409677446469e-01,
                1.71892309775004937e+00,
                8.60091090790866875e-01,
                -9.94640930466322848e-01
            ]
            for i ∈ 1:18
               @test correct_grad[i] ≈ output.gradient[i] atol=tolerance
            end
            test.calculate_jacobian!(test.context, 3)
            test.output!(output, test.context)
            # Gradient is calculated correctly for times = 3
            @test 18 == size(output.gradient, 1)
            for i ∈ 1:18
               @test correct_grad[i] ≈ output.gradient[i] atol=tolerance
            end
            # Objective runs multiple times
            @test can_objective_run_multiple_times!(test.context, test.calculate_objective!)
            # Jacobian runs multiple times
            @test can_objective_run_multiple_times!(test.context, test.calculate_jacobian!)
        finally
            pop!(LOAD_PATH)
        end
    end
end

end

end


using ExtraFrech
using Test

@testset "ExtraFrech.jl" begin
    # Write your tests here.
    @test ExtraFrech.impurity(zeros(10)) == 0

end


struct FixedPolicy <: BanditPolicy
    a::Int64
end

function choose(x::Vector{Float64}, policy::FixedPolicy)
    return policy.a
end

function update!(pol::FixedPolicy, x::Vector{Float64}, a::Int64, r::Float64)
    nothing
end


struct FunctionField{X, Y, Z, C, P, F, G, T} <: AbstractField{X, Y, Z, Nothing, G, T, 3}
          func :: F
          grid :: G
         clock :: C
    parameters :: P

    @doc """
        FunctionField{X, Y, Z}(func, grid; clock=nothing, parameters=nothing) where {X, Y, Z}

    Returns a `FunctionField` on `grid` and at location `X, Y, Z`.

    If `clock` is not specified, then `func` must be a function with signature
    `func(x, y, z)`. If clock is specified, `func` must be a function with signature
    `func(x, y, z, t)`, where `t` is internally determined from `clock.time`.

    A `FunctionField` will return the result of `func(x, y, z [, t])` at `X, Y, Z` on
    `grid` when indexed at `i, j, k`.
    """
    function FunctionField{X, Y, Z}(func::F, grid::G; clock::C=nothing, parameters::P=nothing) where {X, Y, Z, F, G, C, P}
        T = eltype(grid)
        return new{X, Y, Z, C, P, F, G, T}(func, grid, clock, parameters)
    end

    @doc """
        FunctionField{X, Y, Z}(func::FunctionField, grid; clock) where {X, Y, Z}

    Adds `clock` to an existing `FunctionField` and relocates it to `(X, Y, Z)` on `grid`.
    """
    function FunctionField{X, Y, Z}(f::FunctionField, grid::G; clock::C=nothing) where {X, Y, Z, G, C}
        P = typeof(f.parameters)
        T = eltype(grid)
        F = typeof(f.func)
        return new{X, Y, Z, C, P, F, G, T}(f.func, grid, clock, f.parameters)
    end
end

"""Return `a`, or convert `a` to `FunctionField` if `a::Function`"""
fieldify(L, a, grid) = a
fieldify(L, a::Function, grid) = FunctionField(L, a, grid)

"""
    FunctionField(L::Tuple, func, grid)

Returns a stationary `FunctionField` on `grid` and at location `L = (X, Y, Z)`,
where `func` is callable with signature `func(x, y, z)`.
"""
FunctionField(L::Tuple, func, grid) = FunctionField{L[1], L[2], L[3]}(func, grid)

# Ordinary functions needed for fields
architecture(f::FunctionField) = nothing
Base.parent(f::FunctionField) = f

# Various possibilities for calling FunctionField.func:
@inline call_func(clock, parameters, func, x, y, z)     = func(x, y, z, clock.time, parameters)
@inline call_func(::Nothing, parameters, func, x, y, z) = func(x, y, z, parameters)
@inline call_func(clock, ::Nothing, func, x, y, z)      = func(x, y, z, clock.time)
@inline call_func(::Nothing, ::Nothing, func, x, y, z)  = func(x, y, z)

# For setting ReducedField
@inline call_func(::Nothing, ::Nothing, func, x, y)     = func(x, y)
@inline call_func(::Nothing, ::Nothing, func, x)        = func(x)

@inline Base.getindex(f::FunctionField{LX, LY, LZ}, i, j, k) where {LX, LY, LZ} =
    call_func(f.clock, f.parameters, f.func, node(LX(), LY(), LZ(), i, j, k, f.grid)...)

@inline (f::FunctionField)(x...) = call_func(f.clock, f.parameters, f.func, x...)

Adapt.adapt_structure(to, f::FunctionField{X, Y, Z}) where {X, Y, Z} =
    FunctionField{X, Y, Z}(Adapt.adapt(to, f.func),
                           Adapt.adapt(to, f.grid),
                           clock = Adapt.adapt(to, f.clock),
                           parameters = Adapt.adapt(to, f.parameters))

Base.show(io::IO, field::FunctionField) =
    print(io, "FunctionField located at ", show_location(field), '\n',
          "├── func: $(short_show(field.func))", '\n',
          "├── grid: $(short_show(field.grid))\n",
          "├── clock: $(short_show(field.clock))\n",
          "└── parameters: $(field.parameters)")


@testset "evaluate.jl" begin
    @testset "evaluate erroring" begin
        @test_throws DexError evaluate("(1 : Int) + 2.0")
    end

    @testset "evaluate  show" begin
        @test repr(evaluate("1")) == repr("1")
        @test repr(evaluate("1.5")) == repr("1.5")
        @test repr(evaluate("[1, 2]")) == repr("[1, 2]")
        @test repr(evaluate("1+3")) == repr("4")
        @test repr(evaluate("for i. [1, 2].i + 1")) == repr("[2, 3]")

        # This seems weird: why is it doubly quoted? 😕
        @test repr(evaluate("IToW8 65")) === repr(repr('A'))
    end

    @testset "evaluate juliaize" begin
        @test juliaize(evaluate("1")) === Int32(1)
        @test juliaize(evaluate("1.5")) === 1.5f0
        @test juliaize(evaluate("IToW8 65")) === UInt8(65)
    end

    @testset "juliaize-dexize round-trip" begin
        @test juliaize(dexize(Int64(3))) === Int64(3)
        @test juliaize(dexize(Int32(3))) === Int32(3)
        @test juliaize(dexize(UInt8(3))) === UInt8(3)
        @test juliaize(dexize(Float64(3))) === Float64(3)
        @test juliaize(dexize(Float32(3))) === Float32(3)
        @test juliaize(dexize(UInt64(3))) === UInt64(3)
        @test juliaize(dexize(UInt32(3))) === UInt32(3)
    end

    @testset "Atom function call" begin
        m = DexModule("""
        def addOne (x: Float) : Float = x + 1.0
        """)
        x = evaluate("2.5")
        y = evaluate("[2, 3, 4]")
        @test repr(m.addOne(x)) == repr("3.5")

        # This is a function that is in `m` from dex's prelude
        @test repr(m.sum(y)) == repr("9")
    end

    @testset "convert Atom" begin
        atom = convert(Atom, 1f0)
        @test convert(Number, atom) === 1f0
        @test convert(Real, atom) === 1f0
        @test convert(Float64, atom) === 1.0

        atom = convert(Atom, Int32(2))
        @test convert(Number, atom) === Int32(2)
        @test convert(Real, atom) === Int32(2)
        @test convert(Float64, atom) === 2.0
    end

    @testset "DexModule" begin
        m = DexModule("""
        x = 2.5
        y = [2, 3, 4]
        """)
        @test repr(m.x) == repr("2.5")
        @test repr(m.y) == repr("[2, 3, 4]")
    end
end


using PyMNE
using PyCall
using Test

using PyCall: PyError

@testset "sklearn" begin
    PyMNE.install_sklearn()
    # requires sklearn
    @test PyMNE.preprocessing.ICA(method="fastica") isa PyObject
end

@testset "create_info and get_info" begin
    dat = zeros(1, 100)
    naive_info = PyMNE.mne.create_info([:a], 100)
    wrapped_info = PyMNE.create_info([:a], 100)
    raw = PyMNE.io.RawArray(dat, wrapped_info)
    @test raw.get_data() == dat
    @test get_info(raw) isa PyObject
    @test raw.info isa Dict
    @test_throws PyError PyMNE.io.RawArray(dat, naive_info)
end


@nospecialize
export TagfulPattern,
    And,
    Or,
    Literal,
    Wildcard,
    Deconstrucution,
    Guard,
    Effect,
    untagless,
    TagfulPattern,
    PatternInfo

abstract type TagfulPattern end

struct PatternInfo
    pattern::TagfulPattern
    typetag::TypeObject
end

struct And <: TagfulPattern
    ps::Vector{PatternInfo}
end

struct Or <: TagfulPattern
    ps::Vector{PatternInfo}
end

struct Literal{T} <: TagfulPattern
    val::T
end

struct Wildcard <: TagfulPattern end

struct Deconstrucution <: TagfulPattern
    comp::PComp
    extract::Function
    params::Vector{PatternInfo}
end

struct Guard <: TagfulPattern
    predicate::Any
end

struct Effect <: TagfulPattern
    perform::Any
end

@specialize
function _uncurry_call_argtail(f)
    function (_, args...)
        f(args...)
    end
end
@nospecialize

function untagless(points_of_view::Dict{Function, Int})
    myviewpoint = points_of_view[untagless]
    typetag_viewpoint::Int = points_of_view[tag_extract]
    mk_info(all_info)::PatternInfo =
        PatternInfo(all_info[myviewpoint], all_info[typetag_viewpoint])
    ! = mk_info
    function decons(comp::PComp, extract::Function, ps)
        Deconstrucution(comp, extract, PatternInfo[!p for p in ps])
    end
    and(ps::Vector{Vector{Any}}) = And(PatternInfo[!e for e in ps])
    or(ps::Vector{Vector{Any}}) = Or(PatternInfo[!e for e in ps])

    (
        and = and,
        or = or,
        literal = Literal,
        wildcard = Wildcard(),
        decons = decons,
        guard = Guard,
        effect = Effect,
    )
end
@specialize


module Result

using ..FEStructure
using ..LoadCase

include("../util/mmp.jl")

include("./nodal_result.jl")
include("./beam_result.jl")
include("./quad_result.jl")
include("./tria_result.jl")

include("./modal_result.jl")

end