@naiveVariationalRule(:node_type     => NAutoRegressiveMovingAverageX,
                      :outbound_type => Message{GaussianMeanPrecision},
                      :inbound_types => (Nothing, ProbabilityDistribution, ProbabilityDistribution, ProbabilityDistribution, ProbabilityDistribution, ProbabilityDistribution, ProbabilityDistribution),
                      :name          => VariationalNARMAXOutNPPPPPP)

@naiveVariationalRule(:node_type     => NAutoRegressiveMovingAverageX,
                      :outbound_type => Message{GaussianWeightedMeanPrecision},
                      :inbound_types => (ProbabilityDistribution, Nothing, ProbabilityDistribution, ProbabilityDistribution, ProbabilityDistribution, ProbabilityDistribution, ProbabilityDistribution),
                      :name          => VariationalNARMAXIn1PNPPPPP)

@naiveVariationalRule(:node_type     => NAutoRegressiveMovingAverageX,
                      :outbound_type => Message{GaussianWeightedMeanPrecision},
                      :inbound_types => (ProbabilityDistribution, ProbabilityDistribution, Nothing, ProbabilityDistribution, ProbabilityDistribution, ProbabilityDistribution, ProbabilityDistribution),
                      :name          => VariationalNARMAXIn2PPNPPPP)

@naiveVariationalRule(:node_type     => NAutoRegressiveMovingAverageX,
                      :outbound_type => Message{GaussianWeightedMeanPrecision},
                      :inbound_types => (ProbabilityDistribution, ProbabilityDistribution, ProbabilityDistribution, Nothing, ProbabilityDistribution, ProbabilityDistribution, ProbabilityDistribution),
                      :name          => VariationalNARMAXIn3PPPNPPP)

@naiveVariationalRule(:node_type     => NAutoRegressiveMovingAverageX,
                      :outbound_type => Message{GaussianWeightedMeanPrecision},
                      :inbound_types => (ProbabilityDistribution, ProbabilityDistribution, ProbabilityDistribution, ProbabilityDistribution, Nothing, ProbabilityDistribution, ProbabilityDistribution),
                      :name          => VariationalNARMAXIn4PPPPNPP)

@naiveVariationalRule(:node_type     => NAutoRegressiveMovingAverageX,
                      :outbound_type => Message{GaussianWeightedMeanPrecision},
                      :inbound_types => (ProbabilityDistribution, ProbabilityDistribution, ProbabilityDistribution, ProbabilityDistribution, ProbabilityDistribution, Nothing, ProbabilityDistribution),
                      :name          => VariationalNARMAXIn5PPPPPNP)

@naiveVariationalRule(:node_type     => NAutoRegressiveMovingAverageX,
                      :outbound_type => Message{Gamma},
                      :inbound_types => (ProbabilityDistribution, ProbabilityDistribution, ProbabilityDistribution, ProbabilityDistribution, ProbabilityDistribution, ProbabilityDistribution, Nothing),
                      :name          => VariationalNARMAXIn6PPPPPPN)


module Scenarios

using Random
using Statistics
using Distributions
using Parameters

using POMDPs
using ParticleFilters

import POMDPs: initialstate, generate_s, generate_o, generate_sor, support, discount, isterminal, generate_sr
import POMDPs: actions, n_actions, actionindex, action, dimensions, isterminal_act
import POMDPs: states, n_states, stateindex, transition
import POMDPs: observations, observation, n_observations, obsindex, isterminal_obs
import POMDPs: initialstate, initialstate_distribution
import POMDPs: updater, update
import POMDPs: reward
import POMDPs: convert_s, convert_a, convert_o
import POMDPs: solve
import POMDPs: max_possible_weight, new_particle, reset_distribution

include("LightDark/LightDark.jl")

end # module


module GymWrappers

import ..AbstractEnvironment,
    ..reset!,
    ..set_render_mode!,
    ..render,
    ..close,
    ..step!,
    ..observation_type,
    ..observation_extrema,
    ..observation_length,
    ..action_type,
    ..action_set,
    ..action_length,
    ..fps

using PyCall
using StaticArrays

export GymCartPoleV1,
    GymLunarLander,
    GymMountainCarDiscrete,
    GymAcrobot,
    GymBreakoutRAM,
    GymInvertedPendulum,
    GymHopperV2,
    GymCartPoleBulletV1,
    GymReacherBulletV0,
    GymHopperBulletV0,
    GymHalfCheetahBulletV0

const gym = PyNULL()
const pybullet_envs = PyNULL()

abstract type AbstractGymEnvironment{O <: AbstractVector,A} <:
              AbstractEnvironment{O,A} end

reset!(env::AbstractGymEnvironment) = 0.0, env.env.reset(), false
set_render_mode!(env::AbstractGymEnvironment, mode::Symbol) = nothing
function render(env::AbstractGymEnvironment, mode::Union{Nothing,Symbol}=nothing)
    isnothing(mode) && return env.env.render()

    env.env.render(mode="human")
end
close(env::AbstractGymEnvironment) = env.env.close()

# --------------------------------- CartPole --------------------------------- #
struct GymCartPoleV1 <: AbstractGymEnvironment{SizedVector{4,Float64},Int}
    env::PyObject

    GymCartPoleV1() = new(gym.make("CartPole-v1"))
end

observation_type(::Type{GymCartPoleV1}) = SizedVector{4,Float64}
observation_extrema(::Type{GymCartPoleV1}) =
    (SVector(-4.8, -4.0, -0.418, -3.0), SVector(4.8, 4.0, 0.418, 3.0))
observation_length(::Type{GymCartPoleV1}) = 4
action_type(::Type{GymCartPoleV1}) = Int
action_set(::Type{GymCartPoleV1}) = SA[-1, 1]

function step!(env::GymCartPoleV1, action::Int)
    (action != -1 && action != 1) &&
        throw(DomainError(action, "Invalid action"))

    obs, reward, done, _ = env.env.step(action == -1 ? 0 : 1)

    return reward, SizedVector{4}(obs), done
end

reset!(env::GymCartPoleV1) = 0.0, SizedVector{4}(env.env.reset()), false

# -------------------------------- LunarLander ------------------------------- #
struct GymLunarLander <: AbstractGymEnvironment{SizedVector{8,Float32},Int}
    env::PyObject

    GymLunarLander() = new(gym.make("LunarLander-v2"))
end

observation_type(::Type{GymLunarLander}) = SizedVector{8,Float32}
observation_extrema(::Type{GymLunarLander}) =
    ((@SVector fill(-1.0f0, 8)), (@SVector fill(1.0f0, 8)))
observation_length(::Type{GymLunarLander}) = 8
action_type(::Type{GymLunarLander}) = Int
action_set(::Type{GymLunarLander}) = SA[0,1,2,3]

function step!(env::GymLunarLander, action::Int)
    # (action != -1 && action != 1) &&
    #     throw(DomainError(action, "Invalid action"))

    obs, reward, done, _ = env.env.step(action)

    return Float64(reward), SizedVector{8}(obs), done
end

reset!(env::GymLunarLander) = 0.0, SizedVector{8}(env.env.reset()), false
fps(env::GymLunarLander) = 50

# -------------------------- MontainCar (Discrete) --------------------------- #
struct GymMountainCarDiscrete <: AbstractGymEnvironment{Vector{Float64},Int}
    env::PyObject

    function GymMountainCarDiscrete(steps::Int)
        env = gym.make("MountainCar-v0")
        env._max_episode_steps = steps

        new(env)
    end
end

# TODO: Validate
GymMountainCarDiscrete(; steps=200) = GymMountainCarDiscrete(steps)

observation_type(::GymMountainCarDiscrete) = Vector{Float64}
observation_extrema(::GymMountainCarDiscrete) = ([-1.2, -0.07], [0.6, 0.07])
observation_length(::GymMountainCarDiscrete) = 2
action_type(::GymMountainCarDiscrete) = Int
action_set(::GymMountainCarDiscrete) = SA[0,1,2]

function step!(env::GymMountainCarDiscrete, action::Int)
    (action != 0 && action != 1 && action != 2) &&
        throw(DomainError(action, "Invalid action"))

    obs, reward, done, _ = env.env.step(action)

    return reward, obs, done
end

# ------------------------- MontainCar (Continuous) -------------------------- #
struct GymMountainCarContinuous <: AbstractGymEnvironment{SizedVector{2,Float64},SVector{1,Float64}}
    env::PyObject

    function GymMountainCarContinuous()
        env = gym.make("MountainCarContinuous-v0")
        env._max_episode_steps = 999

        new(env)
    end
end

# TODO: Validate
# GymMountainCarContinuous() = GymMountainCarDiscrete(steps)

observation_type(::Type{GymMountainCarContinuous}) = SizedVector{2,Float64}
observation_extrema(::Type{GymMountainCarContinuous}) =
    (SVector(-1.2, -0.07), SVector(0.6, 0.07))
observation_length(::Type{GymMountainCarContinuous}) = 2
action_type(::Type{GymMountainCarContinuous}) = SVector{1,Float64}
# action_set(::GymMountainCarContinuous) = SA[0,1,2]

function step!(env::GymMountainCarContinuous, action::SVector{1,Float64})
    # (action != 0 && action != 1 && action != 2) &&
    #     throw(DomainError(action, "Invalid action"))

    obs, reward, done, _ = env.env.step(clamp.(action, -1.0, 1.0))

    return Float64(reward), SizedVector{2}(obs), done
end

reset!(env::GymMountainCarContinuous) = 0.0, SizedVector{2}(env.env.reset()), false

# --------------------------------- Acrobot ---------------------------------- #
struct GymAcrobot <: AbstractGymEnvironment{Vector{Float64},Int}
    env::PyObject

    function GymAcrobot(steps::Int)
        env = gym.make("Acrobot-v1")
        env._max_episode_steps = steps

        new(env)
    end
end

# TODO: Validate
GymAcrobot(; steps=500) = GymAcrobot(steps)

observation_type(::GymAcrobot) = Vector{Float64}
observation_extrema(::GymAcrobot) = (fill(-1, 6), fill(1, 6))
observation_length(::GymAcrobot) = 6
action_type(::GymAcrobot) = Int
action_set(::GymAcrobot) = SA[-1, 0, 1]

function step!(env::GymAcrobot, action::Int)
    (action != 0 && action != 1 && action != -1) &&
        throw(DomainError(action, "Invalid action"))

    obs, reward, done, _ = env.env.step(action)

    return reward, obs, done
end
# =============================== Atari ====================================== #
# ----------------------------- Breakout (RAM) ------------------------------- #
struct GymBreakoutRAM <: AbstractGymEnvironment{Vector{UInt8},Int}
    env::PyObject

    GymBreakoutRAM() = new(gym.make("Breakout-ram-v0"))
end

observation_type(::GymBreakoutRAM) = Vector{UInt8}
observation_extrema(::GymBreakoutRAM) = (typemin(UInt8), typemax(UInt8))
observation_length(::GymBreakoutRAM) = 128
action_type(::GymBreakoutRAM) = Int
action_set(::GymBreakoutRAM) = 0:3

function step!(env::GymBreakoutRAM, action::Int)
    !(action in action_set(env)) && throw(DomainError(action, "Invalid action"))

    obs, reward, done, _ = env.env.step(action)

    return reward, obs, done
end

# ================================== Mujoco ================================== #
# --------------------------- InvertedPendulum-v2 ---------------------------- #
struct GymInvertedPendulum <: AbstractGymEnvironment{Vector{Float64},Float64}
    env::PyObject

    GymInvertedPendulum() = new(gym.make("InvertedPendulum-v2"))
end

observation_type(::GymInvertedPendulum) = Vector{Float64}
observation_extrema(::GymInvertedPendulum) =
    ([-4.8, -4.0, -0.418, -3.0], [4.8, 4.0, 0.418, 3.0])
observation_length(::GymInvertedPendulum) = 4
action_type(::GymInvertedPendulum) = Float64
action_set(::GymInvertedPendulum) = -3.0:1:3.0

# -------------------------------- Hopper-v2 --------------------------------- #
struct GymHopperV2 <: AbstractGymEnvironment{SizedVector{11,Float32},SVector{3,Float32}}
    env::PyObject

    function GymHopperV2()
        env = gym.make("Hopper-v2")

        new(env)
    end
end

observation_type(::Type{GymHopperV2}) = SizedVector{11,Float32}
observation_extrema(::Type{GymHopperV2}) =
    ((@SVector fill(-1.0f0, 11)), (@SVector fill(1.0f0, 11)))

observation_length(::Type{GymHopperV2}) = 11
action_type(::Type{GymHopperV2}) = SVector{3,Float32}

function step!(env::GymHopperV2, action::SVector{3,Float32})
    obs, reward, done, _ = env.env.step(clamp.(action, -1.0, 1.0))

    return Float64(reward), SizedVector{11}(obs), done
end

reset!(env::GymHopperV2) = 0.0, SizedVector{11}(env.env.reset()), false

# ================================= pybullet ================================= #
# ---------------------------- CartPoleBulletEnv-v1 -------------------------- #
struct GymCartPoleBulletV1 <:
    AbstractGymEnvironment{SizedVector{4,Float64},Int}
    env::PyObject

    GymCartPoleBulletV1() = new(gym.make("CartPoleBulletEnv-v1"))
end

observation_type(::Type{GymCartPoleBulletV1}) = SizedVector{4,Float64}
# observation_extrema(::Type{GymCartPoleBulletV1}) =
#     (SA_F64[-4.8, -4.0, -0.418, -3.0], SA_F64[4.8, 4.0, 0.418, 3.0])
observation_extrema(::Type{GymCartPoleBulletV1}) = (fill(-0.05, 4), fill(0.05, 4))
observation_length(::Type{GymCartPoleBulletV1}) = 4
action_type(::Type{GymCartPoleBulletV1}) = Int
action_set(::Type{GymCartPoleBulletV1}) = SA[-1, 1]

set_render_mode!(env::GymCartPoleBulletV1, mode::Symbol=:human) = render(env, mode)

function step!(env::GymCartPoleBulletV1, action::Int)
    (action != -1 && action != 1) &&
        throw(DomainError(action, "Invalid action"))

    obs, reward, done, _ = env.env.step(action == -1 ? 0 : 1)
    # @show typeof(obs)
    return reward, SizedVector{4}(obs), done
end

reset!(env::GymCartPoleBulletV1) = 0.0, SizedVector{4}(env.env.reset()), false

# ---------------------------- ReacherBulletEnv-v0 -------------------------- #
struct GymReacherBulletV0 <:
    AbstractGymEnvironment{SizedVector{9,Float64},SVector{2,Float64}}
    env::PyObject

    GymReacherBulletV0() = new(gym.make("ReacherBulletEnv-v0"))
end

observation_type(::Type{GymReacherBulletV0}) = SizedVector{9,Float64}
observation_extrema(::Type{GymReacherBulletV0}) = (fill(-1.0, 9), fill(1.0, 9))
observation_length(::Type{GymReacherBulletV0}) = 9
action_type(::Type{GymReacherBulletV0}) = SVector{2,Float64}
# action_set(::Type{GymReacherBulletV0}) = SA[-1, 1]

set_render_mode!(env::GymReacherBulletV0, mode::Symbol=:human) = render(env, mode)

function step!(env::GymReacherBulletV0, action::StaticArray{Tuple{2},Float64,1})
    # (action != -1 && action != 1) &&
    #     throw(DomainError(action, "Invalid action"))

    obs, reward, done, _ = env.env.step(clamp.(action, -1.0f0, 1.0f0))

    return reward, SizedVector{9}(obs), done
end

reset!(env::GymReacherBulletV0) = 0.0, SizedVector{9}(env.env.reset()), false

# ---------------------------- HopperBulletEnv-v0 ---------------------------- #
struct GymHopperBulletV0 <:
       AbstractGymEnvironment{SizedVector{15,Float32},SVector{3,Float32}}
    env::PyObject

    GymHopperBulletV0() = new(gym.make("HopperBulletEnv-v0"))
end

observation_type(::Type{GymHopperBulletV0}) = SizedVector{15,Float32}
observation_extrema(::Type{GymHopperBulletV0}) = (SizedVector{15}(fill(0.0f0, 15)), SizedVector{15}(fill(1.5f0, 15)))
observation_length(::Type{GymHopperBulletV0}) = 15
action_type(::Type{GymHopperBulletV0}) = SVector{3,Float32} # Vector{Float32}
action_length(::Type{GymHopperBulletV0}) = 3
action_set(::Type{GymHopperBulletV0}) = error("Not implemented")
# fps(::Type{GymHopperBulletV0}) = 60

set_render_mode!(env::GymHopperBulletV0, mode::Symbol=:human) = render(env, mode)

function step!(env::GymHopperBulletV0, action::StaticArray{Tuple{3},Float32,1})
    # !env.env.action_space.contains(action) && throw(DomainError(action, "Invalid action"))

    obs, reward, done, _ = env.env.step(clamp.(action, -1.0f0, 1.0f0))

    return reward, SizedVector{15}(obs), done
end

close(env::GymHopperBulletV0) = nothing

reset!(env::GymHopperBulletV0) = 0.0, SizedVector{15}(env.env.reset()), false

# -------------------------- HalfCheetahBulletEnv-v0 ------------------------- #
struct GymHalfCheetahBulletV0 <:
    AbstractGymEnvironment{SizedVector{26,Float32},SVector{6,Float32}}
    env::PyObject

    GymHalfCheetahBulletV0() = new(gym.make("HalfCheetahBulletEnv-v0"))
end

observation_type(::Type{GymHalfCheetahBulletV0}) = SizedVector{26,Float32}
observation_extrema(::Type{GymHalfCheetahBulletV0}) = (SizedVector{26}(fill(-0.5f0, 26)), SizedVector{26}(fill(1.4f0, 26)))
observation_length(::Type{GymHalfCheetahBulletV0}) = 26
action_type(::Type{GymHalfCheetahBulletV0}) = SVector{6,Float32} # Vector{Float32}
action_length(::Type{GymHalfCheetahBulletV0}) = 6
action_set(::Type{GymHalfCheetahBulletV0}) = error("Not implemented")

set_render_mode!(env::GymHalfCheetahBulletV0, mode::Symbol=:human) = render(env, mode)

function step!(env::GymHalfCheetahBulletV0, action::StaticArray{Tuple{6},Float32,1})
    # !env.env.action_space.contains(action) && throw(DomainError(action, "Invalid action"))

    obs, reward, done, _ = env.env.step(clamp.(action, -1, 1))

    return reward, SizedVector{26}(obs), done
end

close(env::GymHalfCheetahBulletV0) = nothing

reset!(env::GymHalfCheetahBulletV0) = 0.0, SizedVector{26}(env.env.reset()), false

function __init__()
    copy!(gym, pyimport_conda("gym", "gym", "conda-forge"))
    copy!(pybullet_envs, pyimport_conda("pybullet_envs", "pybullet"))
end

end


# [
#  [ MPIStateArray Name, Field Name, Maximum, Minimum, Mean, Standard Deviation ],
#  [         :                :          :        :      :          :           ],
# ]

parr = [
    ["3D state", "u[1]", 12, 12, 0, 12],
    ["3D state", "u[2]", 0, 0, 0, 0],
    ["3D state", "η", 12, 12, 0, 12],
    ["3D state", "θ", 15, 15, 15, 15],
    ["3D aux", "y", 15, 15, 15, 15],
    ["3D aux", "w", 12, 12, 0, 12],
    ["3D aux", "pkin", 15, 15, 15, 15],
    ["3D aux", "wz0", 12, 12, 0, 12],
    ["3D aux", "uᵈ[1]", 12, 12, 0, 12],
    ["3D aux", "uᵈ[2]", 0, 0, 0, 0],
    ["3D aux", "ΔGᵘ[1]", 11, 11, 0, 12],
    ["3D aux", "ΔGᵘ[2]", 0, 0, 0, 0],
    ["2D state", "η", 12, 12, 0, 12],
    ["2D state", "U[1]", 12, 12, 0, 12],
    ["2D state", "U[2]", 0, 0, 0, 0],
    ["2D aux", "y", 15, 15, 15, 2],
    ["2D aux", "Gᵁ[1]", 11, 11, 0, 12],
    ["2D aux", "Gᵁ[2]", 0, 0, 0, 0],
    ["2D aux", "Δu[1]", 12, 12, 0, 12],
    ["2D aux", "Δu[2]", 0, 0, 0, 0],
]

uncoupled = [
    [
        "3D state",
        "u[1]",
        -9.58547428246830479637e-01,
        9.58547428246829258391e-01,
        -6.82121026329696195717e-18,
        4.45400655325317695876e-01,
    ],
    [
        "3D state",
        "u[2]",
        -4.13574641907576004779e-14,
        1.84325494853789287544e-14,
        1.94567057477529826177e-17,
        2.12815907156924710074e-15,
    ],
    [
        "3D state",
        "η",
        -8.52721734395889496838e-01,
        8.52834259382379111791e-01,
        2.18869899981655176688e-14,
        6.02983573168680453414e-01,
    ],
    [
        "3D state",
        "θ",
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
    ],
    [
        "3D aux",
        "y",
        0.00000000000000000000e+00,
        1.00000000000000011642e+06,
        5.00000000000000000000e+05,
        2.92775877460665535182e+05,
    ],
    [
        "3D aux",
        "w",
        -4.04489222828922911912e-04,
        4.04649979048336849198e-04,
        7.79376563286859913379e-19,
        1.63949585575251901345e-04,
    ],
    [
        "3D aux",
        "pkin",
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
    ],
    [
        "3D aux",
        "wz0",
        -2.00933099660541601228e-04,
        2.00809859469744893647e-04,
        2.62012633811536956698e-19,
        1.42064682697776552755e-04,
    ],
    [
        "3D aux",
        "uᵈ[1]",
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
    ],
    [
        "3D aux",
        "uᵈ[2]",
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
    ],
    [
        "3D aux",
        "ΔGᵘ[1]",
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
    ],
    [
        "3D aux",
        "ΔGᵘ[2]",
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
    ],
    [
        "2D state",
        "η",
        -8.52715894965927923010e-01,
        8.52861218874714666072e-01,
        -7.70938868299708696148e-16,
        6.03458382295490314284e-01,
    ],
    [
        "2D state",
        "U[1]",
        -3.15423945317149438949e+01,
        3.15423945317147342848e+01,
        1.42724412149908289268e-14,
        2.24262219407601044452e+01,
    ],
    [
        "2D state",
        "U[2]",
        -9.38673083374999972374e-13,
        1.16534781393478256382e-12,
        1.93385973223830695204e-14,
        2.16634164433978071172e-13,
    ],
    [
        "2D aux",
        "y",
        0.00000000000000000000e+00,
        1.00000000000000011642e+06,
        5.00000000000000000000e+05,
        2.92775877460665535182e+05,
    ],
    [
        "2D aux",
        "Gᵁ[1]",
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
    ],
    [
        "2D aux",
        "Gᵁ[2]",
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
    ],
    [
        "2D aux",
        "Δu[1]",
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
    ],
    [
        "2D aux",
        "Δu[2]",
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
    ],
]

coupled = [
    [
        "3D state",
        "u[1]",
        -9.58544854429326798062e-01,
        9.58544854429326798062e-01,
        -4.09272615797817732838e-17,
        4.45400401208546903309e-01,
    ],
    [
        "3D state",
        "u[2]",
        -4.09119364541065205420e-15,
        3.41954410407034240802e-15,
        1.57855184364031418231e-19,
        7.69982545727116047928e-16,
    ],
    [
        "3D state",
        "η",
        -8.52733075123627615177e-01,
        8.52843573070954374948e-01,
        7.27595761418342649852e-17,
        6.02992194663175995473e-01,
    ],
    [
        "3D state",
        "θ",
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
    ],
    [
        "3D aux",
        "y",
        0.00000000000000000000e+00,
        1.00000000000000011642e+06,
        5.00000000000000000000e+05,
        2.92775877460665535182e+05,
    ],
    [
        "3D aux",
        "w",
        -4.04456725919283483547e-04,
        4.04616610112968017130e-04,
        1.33892896769793873666e-18,
        1.63945200913093809460e-04,
    ],
    [
        "3D aux",
        "pkin",
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
    ],
    [
        "3D aux",
        "wz0",
        -2.00813442787548966564e-04,
        2.00686991710668984502e-04,
        1.68642877440561282675e-18,
        1.41979510156587984725e-04,
    ],
    [
        "3D aux",
        "uᵈ[1]",
        -8.79713597641291755735e-01,
        8.79713597641291200624e-01,
        -9.26547727431170607429e-17,
        4.41871673548253185437e-01,
    ],
    [
        "3D aux",
        "uᵈ[2]",
        -5.88884665747485312740e-28,
        6.47654803186450692243e-28,
        1.40221555861782666734e-30,
        4.22950073030020620100e-29,
    ],
    [
        "3D aux",
        "ΔGᵘ[1]",
        -1.50292385646252865781e-09,
        1.50292385640701573972e-09,
        -3.03178502810731714103e-21,
        6.72141424487058536370e-10,
    ],
    [
        "3D aux",
        "ΔGᵘ[2]",
        -2.50330922252795548175e-19,
        2.79421319642066487612e-19,
        8.87468518373638336981e-36,
        5.19465794381941235415e-20,
    ],
    [
        "2D state",
        "η",
        -8.52733075123627615177e-01,
        8.52843573070954374948e-01,
        1.61115565333602722195e-16,
        6.03463098440266798583e-01,
    ],
    [
        "2D state",
        "U[1]",
        -3.15402749945522060671e+01,
        3.15402749945527389741e+01,
        2.12974776703234167348e-14,
        2.24262621677375086904e+01,
    ],
    [
        "2D state",
        "U[2]",
        -1.63647745816416607002e-12,
        1.36781764162808076476e-12,
        6.31420737450488849608e-17,
        3.08233543917742288569e-13,
    ],
    [
        "2D aux",
        "y",
        0.00000000000000000000e+00,
        1.00000000000000011642e+06,
        5.00000000000000000000e+05,
        2.92775877460665535182e+05,
    ],
    [
        "2D aux",
        "Gᵁ[1]",
        -6.01169542562806285963e-07,
        6.01169542585011466433e-07,
        1.21270571032004377760e-18,
        2.69066532005499711718e-07,
    ],
    [
        "2D aux",
        "Gᵁ[2]",
        -1.11768527856826595815e-16,
        1.00132368901118217729e-16,
        -3.50057026691823957451e-33,
        2.07948587451660775232e-17,
    ],
    [
        "2D aux",
        "Δu[1]",
        -6.53936429129693404076e-04,
        6.53936429129569046087e-04,
        -2.70378337774435083119e-18,
        4.64975945389895016328e-04,
    ],
    [
        "2D aux",
        "Δu[2]",
        -3.76049176138251460297e-17,
        3.57641761985933683411e-17,
        -4.12443309266433447246e-19,
        7.09162842149286857838e-18,
    ],
]

thirty_minutes = [
    [
        "3D state",
        "u[1]",
        -9.58527737426365766815e-01,
        9.58527737426364989659e-01,
        -2.95585778076201651428e-17,
        4.45397585011924779241e-01,
    ],
    [
        "3D state",
        "u[2]",
        -3.91776258115697290002e-15,
        3.71573344368902785991e-15,
        9.99453214450878842160e-18,
        7.55533641450465537744e-16,
    ],
    [
        "3D state",
        "η",
        -8.52729582431237531637e-01,
        8.52830944991449846349e-01,
        -2.54658516496419884307e-16,
        6.02990141900936249542e-01,
    ],
    [
        "3D state",
        "θ",
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
    ],
    [
        "3D aux",
        "y",
        0.00000000000000000000e+00,
        1.00000000000000011642e+06,
        5.00000000000000000000e+05,
        2.92775877460665535182e+05,
    ],
    [
        "3D aux",
        "w",
        -4.06712181304362465229e-04,
        4.06869755985816939844e-04,
        9.07052211118752913371e-19,
        1.64290385445136678539e-04,
    ],
    [
        "3D aux",
        "pkin",
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
    ],
    [
        "3D aux",
        "wz0",
        -2.09003099782708142351e-04,
        2.08868143992394606845e-04,
        1.33781874467331362536e-18,
        1.47768608464728741676e-04,
    ],
    [
        "3D aux",
        "uᵈ[1]",
        -8.79792191807299950312e-01,
        8.79792191807298729067e-01,
        -9.54969436861574689412e-17,
        4.41911149114936674387e-01,
    ],
    [
        "3D aux",
        "uᵈ[2]",
        -4.49650715975976729085e-28,
        3.22939933074851707840e-28,
        1.28559371735991545201e-30,
        3.12162656228539824003e-29,
    ],
    [
        "3D aux",
        "ΔGᵘ[1]",
        -1.52356619979685413105e-09,
        1.52356619973800994984e-09,
        -3.00415481336788178489e-21,
        6.81373145706173027634e-10,
    ],
    [
        "3D aux",
        "ΔGᵘ[2]",
        -2.17115289587171316651e-19,
        2.99271337844227832879e-19,
        3.57452597678270976423e-36,
        5.23982958192948877053e-20,
    ],
    [
        "2D state",
        "η",
        -8.52729582431237531637e-01,
        8.52830944991449846349e-01,
        -3.20454773827805192348e-16,
        6.03461044074932395631e-01,
    ],
    [
        "2D state",
        "U[1]",
        -3.15407643291233448224e+01,
        3.15407643291237640426e+01,
        2.30678864898692384736e-14,
        2.24265574817805202201e+01,
    ],
    [
        "2D state",
        "U[2]",
        -1.56710503246274425174e-12,
        1.48629337747559491236e-12,
        3.99781285780299048032e-15,
        3.02449468686897625244e-13,
    ],
    [
        "2D aux",
        "y",
        0.00000000000000000000e+00,
        1.00000000000000011642e+06,
        5.00000000000000000000e+05,
        2.92775877460665535182e+05,
    ],
    [
        "2D aux",
        "Gᵁ[1]",
        -6.09426479895203986555e-07,
        6.09426479918741655731e-07,
        1.20165633492970096897e-18,
        2.72762104279987029506e-07,
    ],
    [
        "2D aux",
        "Gᵁ[2]",
        -1.19708535137691129300e-16,
        8.68461158348685235787e-17,
        -1.50869648123518501517e-33,
        2.09756864038773538444e-17,
    ],
    [
        "2D aux",
        "Δu[1]",
        -3.89449877638695009935e-03,
        3.89449877638627485824e-03,
        -2.02396849009311295664e-17,
        2.76903473119681107703e-03,
    ],
    [
        "2D aux",
        "Δu[2]",
        -2.13991366909841745553e-16,
        2.02991354131742782465e-16,
        -1.26781868574011981865e-18,
        4.34606820130057938426e-17,
    ],
]

sixty_minutes = [
    [
        "3D state",
        "u[1]",
        -9.58507705628042327994e-01,
        9.58507705628042550039e-01,
        -5.22959453519433752618e-17,
        4.45394100308371843067e-01,
    ],
    [
        "3D state",
        "u[2]",
        -4.09893498131451878178e-15,
        3.66595063838075513859e-15,
        3.52058149555652693147e-18,
        7.65972478919525466697e-16,
    ],
    [
        "3D state",
        "η",
        -8.52715753705294066123e-01,
        8.52819886810596838878e-01,
        0.00000000000000000000e+00,
        6.02986351734545844572e-01,
    ],
    [
        "3D state",
        "θ",
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
    ],
    [
        "3D aux",
        "y",
        0.00000000000000000000e+00,
        1.00000000000000011642e+06,
        5.00000000000000000000e+05,
        2.92775877460665535182e+05,
    ],
    [
        "3D aux",
        "w",
        -4.09355343664679748733e-04,
        4.09526445665209497954e-04,
        8.01581023779363006131e-19,
        1.64782726593626366188e-04,
    ],
    [
        "3D aux",
        "pkin",
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
    ],
    [
        "3D aux",
        "wz0",
        -2.18597874938533713813e-04,
        2.18512725277962983460e-04,
        1.16684439888103955116e-18,
        1.54581525183602542031e-04,
    ],
    [
        "3D aux",
        "uᵈ[1]",
        -8.79886113674857694988e-01,
        8.79886113674856806810e-01,
        -8.81072992342524199517e-17,
        4.41958323665245178535e-01,
    ],
    [
        "3D aux",
        "uᵈ[2]",
        -5.04476548888837049561e-28,
        4.09418809809705127009e-28,
        1.53142189509630563206e-30,
        3.27579623353337388378e-29,
    ],
    [
        "3D aux",
        "ΔGᵘ[1]",
        -1.12461707299293835875e-09,
        1.12461707291060059378e-09,
        -2.89803852573586336546e-21,
        5.02954103857782471789e-10,
    ],
    [
        "3D aux",
        "ΔGᵘ[2]",
        -2.29220407034248886795e-19,
        3.23795504633739099693e-19,
        4.19082355898662551731e-36,
        5.50021671320874203932e-20,
    ],
    [
        "2D state",
        "η",
        -8.52715753705294066123e-01,
        8.52819886810596838878e-01,
        -2.04281036531028799987e-17,
        6.03457250948630341547e-01,
    ],
    [
        "2D state",
        "U[1]",
        -3.15415180428826182890e+01,
        3.15415180428829948767e+01,
        1.77902803599749859788e-14,
        2.24265293235027023400e+01,
    ],
    [
        "2D state",
        "U[2]",
        -1.63957399252576030728e-12,
        1.46638025535227397199e-12,
        1.40823259822193190848e-15,
        3.06628264537953242560e-13,
    ],
    [
        "2D aux",
        "y",
        0.00000000000000000000e+00,
        1.00000000000000011642e+06,
        5.00000000000000000000e+05,
        2.92775877460665535182e+05,
    ],
    [
        "2D aux",
        "Gᵁ[1]",
        -4.49846829164240224277e-07,
        4.49846829197175353425e-07,
        1.15921837490966064206e-18,
        2.01338753352722549629e-07,
    ],
    [
        "2D aux",
        "Gᵁ[2]",
        -1.29518201853495637566e-16,
        9.16881628136995516365e-17,
        -2.08062063752041870574e-33,
        2.20180483211723137251e-17,
    ],
    [
        "2D aux",
        "Δu[1]",
        -7.71677156339491132631e-03,
        7.71677156339359640591e-03,
        -2.97775205101297201914e-17,
        5.48673197909435757247e-03,
    ],
    [
        "2D aux",
        "Δu[2]",
        -5.01794629524824969143e-16,
        5.51655281127317078031e-16,
        3.20748218455203547025e-18,
        9.36761976343254920594e-17,
    ],
]

ninety_minutes = [
    [
        "3D state",
        "u[1]",
        -9.58488051495743675900e-01,
        9.58488051495742121588e-01,
        2.50111042987555274331e-17,
        4.45390687454859546257e-01,
    ],
    [
        "3D state",
        "u[2]",
        -1.23989905776825655281e-15,
        1.61970487656192169626e-15,
        3.83419479315953585092e-17,
        3.55219724018798369983e-16,
    ],
    [
        "3D state",
        "η",
        -8.52731405452109680887e-01,
        8.52809353848989926128e-01,
        -2.45563569478690627377e-16,
        6.02988963894262375298e-01,
    ],
    [
        "3D state",
        "θ",
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
    ],
    [
        "3D aux",
        "y",
        0.00000000000000000000e+00,
        1.00000000000000011642e+06,
        5.00000000000000000000e+05,
        2.92775877460665535182e+05,
    ],
    [
        "3D aux",
        "w",
        -4.11959288170071617242e-04,
        4.12126623290091649143e-04,
        -2.00395255944840751533e-19,
        1.65355949740609838115e-04,
    ],
    [
        "3D aux",
        "pkin",
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
        0.00000000000000000000e+00,
    ],
    [
        "3D aux",
        "wz0",
        -2.28048874139989487609e-04,
        2.27949849600796259491e-04,
        -3.35287353436797287965e-19,
        1.61271245461141072295e-04,
    ],
    [
        "3D aux",
        "uᵈ[1]",
        -8.79979607451056078382e-01,
        8.79979607451055745315e-01,
        -2.84217094304040087969e-18,
        4.42005283454923181274e-01,
    ],
    [
        "3D aux",
        "uᵈ[2]",
        -2.89610559829263959062e-28,
        2.76495747279964637797e-28,
        -2.23087062731398024379e-31,
        2.03879747098428489165e-29,
    ],
    [
        "3D aux",
        "ΔGᵘ[1]",
        -8.86991425991573654091e-10,
        8.86991425890194811807e-10,
        -2.95809316169619294608e-22,
        3.96682558403758137461e-10,
    ],
    [
        "3D aux",
        "ΔGᵘ[2]",
        -1.46453678699776496163e-19,
        1.78310815453102811486e-19,
        3.45126646034192634633e-36,
        3.36942772885638802169e-20,
    ],
    [
        "2D state",
        "η",
        -8.52731405452109680887e-01,
        8.52809353848989926128e-01,
        -2.24886775868071691631e-16,
        6.03459865148300189652e-01,
    ],
    [
        "2D state",
        "U[1]",
        -3.15423820746574818941e+01,
        3.15423820746572900475e+01,
        1.25987324739451618828e-14,
        2.24266774102466719398e+01,
    ],
    [
        "2D state",
        "U[2]",
        -4.95959623107294663097e-13,
        6.47881950624733081946e-13,
        1.53367791726381670695e-14,
        1.42198852443335733848e-13,
    ],
    [
        "2D aux",
        "y",
        0.00000000000000000000e+00,
        1.00000000000000011642e+06,
        5.00000000000000000000e+05,
        2.93004519336559635121e+05,
    ],
    [
        "2D aux",
        "Gᵁ[1]",
        -3.54796570356077906525e-07,
        3.54796570396629450056e-07,
        1.18322201808542668922e-19,
        1.58796938275634264439e-07,
    ],
    [
        "2D aux",
        "Gᵁ[2]",
        -7.13243261812411242092e-17,
        5.85814714799106030876e-17,
        -1.27696859032651283100e-33,
        1.34882362672174716756e-17,
    ],
    [
        "2D aux",
        "Δu[1]",
        -1.14629123365972158261e-02,
        1.14629123365972678678e-02,
        2.41175081207994926868e-18,
        8.15149713200666835300e-03,
    ],
    [
        "2D aux",
        "Δu[2]",
        -7.92579521344275918936e-16,
        6.08374854985514025679e-16,
        -3.41928350044524220733e-18,
        1.39041335489639128848e-16,
    ],
]

refVals = (
    uncoupled = (uncoupled, parr),
    coupled = (coupled, parr),
    thirty_minutes = (thirty_minutes, parr),
    sixty_minutes = (sixty_minutes, parr),
    ninety_minutes = (ninety_minutes, parr),
)


"""
        INFO HERE
"""

"""
                Debug data from shock
"""
mutable struct CRShockData
   """   Data structure to analyze a single shocked particle.
         To be used with compile option: LMB_CR_DEBUG_TRACK_PARTICLE
   """
   dt::Vector{Float64}
   Mach::Vector{Float64}
   Shock_Speed::Vector{Float64}
   Shock_Compress::Vector{Float64}
   Shock_Energy_In::Vector{Float64}
   Shock_Energy_Real::Vector{Float64}
   Energy_P::Vector{Float64}
   Energy_e::Vector{Float64}

   function CRShockData(N::Int64=0)

       new(zeros(N), zeros(N), zeros(N), zeros(N),
           zeros(N), zeros(N), zeros(N), zeros(N))

   end
end

"""
                Distribution spectrum in momentum space
"""

struct CRMomentumDistributionConfig
   """   Config object to obtain distribution spectrum in momentum space.
   """
    pmin::Float64
    pmax::Float64
    Nbins::Int64
    bin_width::Float64
    mp::Float64
    me::Float64
    c::Float64
    mc_e::Float64
    mc_p::Float64

    function CRMomentumDistributionConfig(pmin::Float64=0.0, pmax::Float64=0.0,
                                         Nbins::Int64=24, new::Bool=true)

        if new
            CNST_ME = 1.0
            CNST_MP = 1.0
            CNST_C  = 1.0
        else
            CNST_ME = 9.1095e-28
            CNST_MP = 1.6726e-24
            CNST_C = 2.9979e10
        end

        MCe = CNST_ME * CNST_C
        MCp = CNST_MP * CNST_C
        bin_width = log10(pmax/pmin)/Nbins

        new(pmin, pmax, Nbins, bin_width, CNST_MP, CNST_ME, CNST_C, MCe, MCp)

   end
end

mutable struct CRMomentumDistribution

    CRp_dis::Vector{Float64}
    CRp_bound::Vector{Float64}
    CRe_dis::Vector{Float64}
    CRe_bound::Vector{Float64}

    function CRMomentumDistribution(Nbins::Int64)

        CRp_dis = zeros(Int(2*Nbins))
        CRe_dis = zeros(Int(2*Nbins))
        CRp_bound = zeros(Int(2*Nbins + 1))
        CRe_bound = zeros(Int(2*Nbins + 1))

        new( CRp_dis, CRp_bound,
             CRe_dis, CRe_bound )
    end
end


# Definition of StrBootTest "class" for holding intermediate results, with associated utilities and get functions

struct StrClust{T<:Real}
	N::Int; multiplier::T; even::Bool
	order::Vector{Int64}
	info::Vector{UnitRange{Int64}}
end

struct StrFE{T<:Real}
	is::SubArray{Int64, 1, Vector{Int64}, Tuple{UnitRange{Int64}}, true}
  wt::Vector{T}
  sqrtwt::Vector{T}
end

mutable struct StrEstimator{T<:AbstractFloat}
  isDGP::Bool; liml::Bool; fuller::T; κ::T
  R₁perp::Matrix{T}; Rpar::Matrix{T}

  kZ::Int64
  y₁::Vector{T}; ü₁::Vector{T}; u⃛₁::Vector{T}; β̈::Vector{T}; γ̈::Vector{T}; β̈₀::Vector{T}; invXXXy₁par::Vector{T}
  Yendog::Matrix{Bool}
  invZperpZperp::Symmetric{T,Matrix{T}}; invZperpZperpZperpX::Matrix{T}; XZ::Matrix{T}; YPXY::Symmetric{T,Matrix{T}}; R₁invR₁R₁::Matrix{T}
	restricted::Bool; RperpX::Matrix{T}; RperpXperp::Matrix{T}; RRpar::Matrix{T}; RparX::Matrix{T}; RparY::Matrix{T}; RR₁invR₁R₁::Matrix{T}
	∂β̈∂r::Matrix{T}; YY::Symmetric{T,Matrix{T}}; AR::Matrix{T}; XAR::Matrix{T}; R₁invR₁R₁Y::Matrix{T}; invXXXZ::Matrix{T}; Ü₂::Matrix{T}; Rt₁::Vector{T}
	invXX::Symmetric{T,Matrix{T}}; Y₂::Matrix{T}; X₂::Matrix{T}; invH::Symmetric{T,Matrix{T}}
	y₁par::Vector{T}; Xy₁par::Vector{T}
	A::Symmetric{T,Matrix{T}}; Z::Matrix{T}; Zperp::Matrix{T}; X₁::Matrix{T}
	WXAR::Matrix{T}; CT_XAR::Vector{Matrix{T}}

  # IV/GMM only
  ZZ::Symmetric{T,Matrix{T}}; XY₂::Matrix{T}; XX::Symmetric{T,Matrix{T}}; H_2SLS::Symmetric{T,Matrix{T}}; V::Matrix{T}; ZY₂::Matrix{T}; ZR₁ZR₁::Symmetric{T,Matrix{T}}; X₂ZR₁::Matrix{T}; ZR₁Y₂::Matrix{T}; X₁ZR₁::Matrix{T}
  ZR₁Z::Matrix{T}; X₂y₁::Vector{T}; X₁y₁::Vector{T}; Zy₁::Vector{T}; ZXinvXXXZ::Matrix{T}; H_2SLSmZZ::Symmetric{T,Matrix{T}}
  ZXinvXXXy₁par::Vector{T}; t₁Y::Vector{T}
  Y₂y₁::Vector{T}; twoZR₁y₁::Vector{T}; y₁y₁::T; y₁pary₁par::T; Y₂Y₂::Matrix{T}
  X₂y₁par::Vector{T}; X₁y₁par::Vector{T}; Zy₁par::Vector{T}; Y₂y₁par::Vector{T}
  Rperp::Matrix{T}; ZR₁::Matrix{T}
  kX::Int64; kX₁::Int64; kZperp::Int64
	Π̂::Matrix{T}
	S✻⋂ZperpZpar::Array{T,3}; S✻⋂ZperpY₂::Array{T,3}; S⋂y₁X₁::Matrix{T}; S⋂y₁X₂::Matrix{T}; S⋂Xy₁::Matrix{T}; S✻⋂ZperpZperp::Array{T,3}; ZperpX::Matrix{T}; S✻⋂Zperpy₁::Matrix{T}; S✻⋂XZR₁::Array{T,3}; S✻⋂XY₂::Array{T,3}; S✻⋂XZperp::Array{T,3}; S✻⋂Xy₁::Matrix{T}; S✻⋂XX::Array{T,3}; S✻⋂XZpar::Array{T,3}
	S✻⋂X₁Y₂::Array{T,3}; S✻⋂X₂Y₂::Array{T,3}; S✻ZparY₂::Array{T,3}; S✻y₁y₁::Vector{T}; S✻Zpary₁::Matrix{T}; S✻ZR₁y₁::Matrix{T}; S✻ZR₁Y₂::Array{T,3}; S✻ZR₁ZR₁::Array{T,3}; S✻ZR₁Z::Array{T,3}
	ZperpZR₁::Matrix{T}; S✻⋂ZperpZR₁::Array{T,3}
	S✻Y₂Y₂::Array{T,3}; S✻ZparZpar::Array{T,3}; S✻Y₂y₁::Matrix{T}
	Xpar₁::Matrix{T}
	invZperpZperpZperpX₁::Matrix{T}; invZperpZperpZperpX₂::Matrix{T}; invZperpZperpZperpy₁::Vector{T}; invZperpZperpZperpY₂::Matrix{T}; S✻UY₂::Matrix{T}; invZperpZperpZperpZpar::Matrix{T}; invZperpZperpZperpZR₁::Matrix{T}

  StrEstimator{T}(isDGP, liml, fuller, κ) where T<:AbstractFloat = new(isDGP, liml, fuller, κ, Matrix{T}(undef,0,0))
end

mutable struct StrBootTest{T<:AbstractFloat}
  R::Matrix{T}; r::Vector{T}; R₁::Matrix{T}; r₁::Vector{T}
  y₁::Vector{T}; X₁::Matrix{T}; Y₂::Matrix{T}; X₂::Matrix{T}
  wt::Vector{T}; fweights::Bool
  liml::Bool; fuller::T; κ::T; arubin::Bool
  B::Int64; auxtwtype::Symbol; rng::AbstractRNG; maxmatsize::Float16
  ptype::Symbol; null::Bool; bootstrapt::Bool
	ID::Matrix{Int64}; NBootClustVar::Int8; NErrClustVar::Int8; issorted::Bool; small::Bool; clusteradj::Bool; clustermin::Bool
  FEID::Vector{Int64}; FEdfadj::Int64
  level::T; rtol::T
  madjtype::Symbol; NH₀::Int16
  ml::Bool; β̈::Vector{T}; A::Symmetric{T,Matrix{T}}; sc::Matrix{T}
  willplot::Bool; gridmin::Vector{T}; gridmax::Vector{T}; gridpoints::Vector{Float32}

  q::Int16; twotailed::Bool; scorebs::Bool; robust::Bool

  WRE::Bool; initialized::Bool; NFE::Int64; FEboot::Bool; granular::Bool; NErrClustCombs::Int16; subcluster::Int8; BFeas::Int64; interpolating::Bool
  v_sd::T
  confpeak::Vector{T}
  ID✻::Vector{Int64}; ID✻_✻⋂::Vector{Int64}
  anchor::Vector{T}; poles::Vector{T}; numer::Matrix{T}
  ci::Matrix{T}
  peak::NamedTuple{(:X, :p), Tuple{Vector{T}, T}}

	Nobs::Int64; NClustVar::Int8; kX₁::Int64; kX₂::Int64; kY₂::Int64; WREnonARubin::Bool; boottest!::Function
	coldotplus!::Function; colquadformminus!::Function; matmulplus!::Function; panelsum!::Function

  sqrt::Bool; _Nobs::T; kZ::Int64; sumwt::T; haswt::Bool; sqrtwt::Vector{T}; REst::Bool; multiplier::T; smallsample::T
		dof::Int64; dof_r::T; p::T; BootClust::Int8
		purerobust::Bool; N✻::Int64; N⋂::Int64; N✻⋂::Int64; Nw::Int64; enumerate::Bool; interpolable::Bool; interpolate_u::Bool; kX::Int64
  _FEID::Vector{Int64}; AR::Matrix{T}; v::Matrix{T}; u✻::Matrix{T}
  info✻::Vector{UnitRange{Int64}}; info✻_✻⋂::Vector{UnitRange{Int64}}; infoBootAll::Vector{UnitRange{Int64}}; info⋂_✻⋂::Vector{UnitRange{Int64}}
  JN⋂N✻::Matrix{T}; statDenom::Matrix{T}; uXAR::Matrix{T}; SuwtXA::Matrix{T}; numer₀::Matrix{T}; β̈dev::Matrix{T}
	YY✻_b::Matrix{T}; YPXY✻_b::Matrix{T}; numerw::Matrix{T}; numer_b::Vector{T}; dist::Matrix{T}

	distCDR::Matrix{T}; plotX::Vector{Vector{T}}; plotY::Vector{T}; ClustShare::Vector{T}; WeightGrp::Vector{UnitRange{Int64}}
  numersum::Vector{T}; u✻₀::Matrix{T}; invFEwt::Vector{T}
	β̈s::Matrix{T}; As::Matrix{T}
	info✻⋂::Vector{UnitRange{Int64}}; info⋂::Vector{UnitRange{Int64}}; ID✻⋂::Matrix{T}
	ü::Vector{T}
	DGP::StrEstimator{T}; Repl::StrEstimator{T}; M::StrEstimator{T}
	clust::Vector{StrClust{T}}
	denom::Matrix{Matrix{T}}; Kcd::Matrix{Matrix{T}}; Jcd::Matrix{Matrix{T}}; denom₀::Matrix{Matrix{T}}; Jcd₀::Matrix{Matrix{T}}; S✻UU::Matrix{Vector{T}}; CTUX::Matrix{Matrix{T}}
	∂u∂r::Vector{Matrix{T}}; ∂numer∂r::Vector{Matrix{T}}; S✻XU::Vector{Matrix{T}}; invXXS✻XU::Vector{Matrix{T}}; invZperpZperpS✻ZperpU::Vector{Matrix{T}}; S✻YU::Matrix{Vector{T}}; S✻UMZperp::Vector{Matrix{T}}; S✻UPX::Vector{Matrix{T}}; S✻ZperpU::Vector{Matrix{T}}; CTFEU::Vector{Matrix{T}}
  ∂denom∂r::Array{Matrix{T},3}; ∂Jcd∂r::Array{Matrix{T},3}; ∂²denom∂r²::Array{Matrix{T},4}
	FEs::Vector{StrFE{T}}
  T1L::Vector{Matrix{T}}; T1R::Vector{Matrix{T}}; T2::Matrix{T}; J⋂s::Vector{Array{T,3}}; Q::Array{T,3}; β̈v::Vector{Matrix{T}}
	crosstab⋂✻ind::Vector{Int64}; crosstab✻ind::Vector{Int64}
  seed::UInt64

	S✻XY₂::Array{T,3}; S✻XX::Array{T,3}; S✻XDGPZ::Array{T,3}; S✻Xy₁::Matrix{T}; S✻XZR₁::Array{T,3}
	invXXS✻XY₂::Array{T,3}; invXXS✻XX::Array{T,3}; invXXS✻XDGPZ::Array{T,3}; invXXS✻Xy₁::Matrix{T}; invXXS✻XDGPZR₁::Array{T,3}
	S✻⋂XY₂::Array{T,3}; S✻⋂XX::Array{T,3}; S✻⋂XDGPZ::Array{T,3}; S✻⋂Xy₁::Matrix{T}; S✻⋂X_DGPZR₁::Array{T,3}
	invXXS✻⋂XY₂::Array{T,3}; invXXS✻⋂XX::Array{T,3}; invXXS✻⋂XDGPZ::Array{T,3}; invXXS✻⋂Xy₁::Matrix{T}; invXXS✻⋂XDGPZR₁::Array{T,3}
	S✻ZperpY₂::Array{T,3}; S✻ZperpX::Array{T,3}; S✻ZperpDGPZ::Array{T,3}; S✻Zperpy₁::Matrix{T}; S✻ZperpDGPZR₁::Array{T,3}
	invZperpZperpS✻ZperpY₂::Array{T,3}; invZperpZperpS✻ZperpX::Array{T,3}; invZperpZperpS✻ZperpDGPZ::Array{T,3}; invZperpZperpS✻Zperpy₁::Matrix{T}; invZperpZperpS✻ZperpDGPZR₁::Array{T,3}
	_ID✻⋂::Vector{Int}
	S✻Y₂y₁::Matrix{T}; S✻DGPZy₁::Matrix{T}; S✻y₁y₁::Vector{T}; S✻DGPZR₁y₁::Matrix{T}; r₁S✻ReplZR₁Y₂::Array{T,3}; r₁S✻ReplZR₁X::Array{T,3}; r₁S✻ReplZR₁DGPZ::Array{T,3}; r₁S✻ReplZR₁y₁::Matrix{T}; r₁S✻ReplZR₁DGPZR₁::Array{T,3}; S✻ReplZY₂::Array{T,3}; S✻ReplZX::Array{T,3}; S✻ReplZDGPZ::Array{T,3}; S✻ReplZy₁::Matrix{T}; S✻ReplZDGPZR₁::Array{T,3}
	S✻U_S✻UMZperpX::Matrix{Array{T,3}}; negS✻UMZperpX::Vector{Array{T,3}}; S⋂XZperpinvZperpZperp::Array{T,3}; CT✻FEX::Array{T,3}; CT⋂FEX::Array{T,3}; CT✻FEY₂::Array{T,3}; CT✻FEZ::Array{T,3}; CT✻FEy₁::Matrix{T}; CT✻FEZR₁::Array{T,3}
	S✻Y₂Y₂::Array{T,3}; S✻ZparY₂Z_DGPZ::Array{T,3}; S✻DGPZY₂::Array{T,3}; S✻DGPZR₁Y₂::Array{T,3}; S✻DGPZDGPZ::Array{T,3};  S✻DGPZR₁DGPZR₁::Array{T,3}; S✻DGPZR₁DGPZ::Array{T,3}; S✻DGPZR₁X::Array{T,3}
	XinvXX::Matrix{T}; PXZ::Matrix{T}; FillingT₀::Matrix{Matrix{T}}
	S⋂ReplZX::Array{T,3}; S⋂Xy₁::Matrix{T}
	S✻⋂XU₂::Array{T,3}; S✻⋂XU₂RparY::Array{T,3}; S✻XU₂::Array{T,3}; S✻XU₂RparY::Array{T,3}; S✻ZperpU₂::Array{T,3}; S✻ZperpU₂RparY::Array{T,3}; invZperpZperpS✻ZperpU₂::Array{T,3}; invZperpZperpS✻ZperpU₂RparY::Array{T,3}; invXXS✻XU₂::Array{T,3}; invXXS✻XU₂RparY::Array{T,3}

	StrBootTest{T}(R, r, R₁, r₁, y₁, X₁, Y₂, X₂, wt, fweights, liml,
	               fuller, κ, arubin, B, auxtwtype, rng, maxmatsize, ptype, null, scorebs, bootstrapt, ID, NBootClustVar, NErrClustVar, issorted, robust, small, clusteradj, clustermin,
								 NFE, FEID, FEdfadj, level, rtol, madjtype, NH₀, ml,
								 β̈, A, sc, willplot, gridmin, gridmax, gridpoints) where T<:Real =
		begin
			kX₂ = ncols(X₂)
			scorebs = scorebs || iszero(B) || ml
			WREnonARubin = !(iszero(kX₂) || scorebs) && !arubin

			new(R, r, R₁, r₁, y₁, X₁, Y₂, X₂, wt, fweights, liml || !iszero(fuller),
					fuller, κ, arubin, B, auxtwtype, rng, maxmatsize, ptype, null, bootstrapt, ID, NBootClustVar, NErrClustVar, issorted, small, clusteradj, clustermin,
					FEID, FEdfadj, level, rtol, madjtype, NH₀, ml,
					β̈, A, sc, willplot, gridmin, gridmax, gridpoints,
				nrows(R), ptype == :symmetric || ptype == :equaltail, scorebs, robust || NErrClustVar>0,
				false, false, NFE, false, false, 0, 0, 0, false,
				one(T),
				[zero(T)],
				Vector{Int64}(undef,0), Vector{Int64}(undef,0),
				Vector{T}(undef,0), Vector{T}(undef,0), Matrix{T}(undef,0,0),
				Matrix{T}(undef,0,0),
				(X = Vector{T}(undef,0), p = T(NaN)),
				nrows(X₁), ncols(ID), ncols(X₁), kX₂, ncols(Y₂), WREnonARubin, WREnonARubin ? boottestWRE! : boottestOLSARubin!,
				coldotplus_nonturbo!,
				colquadformminus_nonturbo!,
				matmulplus_nonturbo!,
				panelsum_nonturbo!)
		end
end

function getdist(o::StrBootTest, diststat::Symbol=:none)
  if diststat == :numer
	  _numer = isone(o.v_sd) ? o.numer : o.numer / o.v_sd
	  o.distCDR = (@view _numer[:,2:end]) .+ o.r
	  # sort!(o.distCDR, dims=1)
  elseif nrows(o.distCDR)==0  # return test stats
    if length(o.dist) > 1
	    o.distCDR = (@view o.dist[1,2:end])' * o.multiplier
	    # sort!(o.distCDR, dims=1)
	  else
	    o.distCDR = zeros(0,1)
	  end
  end
  o.distCDR
end

function sumgreater(x, v)
  dest = zero(Int64)
  @inbounds @simd for i in v
	  x > i && (dest += 1)
  end
  dest
end
function sumless(x, v)
  dest = zero(Int64)
  @inbounds @simd for i in v
	  x < i && (dest += 1)
  end
  dest
end
function sumlessabs(x, v)
  dest = zero(Int64)
  @inbounds @simd for i in v
	  x < abs(i) && (dest += 1)
  end
  dest
end

# store p value in o.p. Return optionally-multiple-hypothesis-adjusted p value. Robust to missing bootstrapped values interpreted as +infinity.
function getp(o::StrBootTest{T}) where T
  o.boottest!(o)
  tmp = o.dist[1]
  isnan(tmp) && return tmp
  if o.B>0
  	if o.sqrt && o.ptype ≠ :upper
  	  if o.ptype == :symmetric
  	    n = sumlessabs(abs(tmp), o.dist)   # symmetric p value; do so as not to count missing entries in *dist
  	  elseif o.ptype == :equaltail
  	    n = 2min(sumgreater(tmp, o.dist) , sumless(tmp, o.dist))
  	  else
  		  n = sumgreater(tmp,  o.dist)  # lower-tailed p value
      end
  	else
  	  n = sumless(tmp, o.dist)  # upper-tailed p value or p value based on squared stats
    end
    o.p = n / o.BFeas |> T
  else
		tmp *= o.multiplier
		o.p = ccdf(o.small ? FDist{T}(T(o.dof), o.dof_r) : Chisq{T}(T(o.dof)), o.sqrt ? tmp^2 : tmp)
		if o.sqrt && !o.twotailed
			o.p /= 2
			(o.ptype == :upper) == (tmp<0) && (o.p = 1 - o.p)
		end
  end

	if o.madjtype == :bonferroni min(one(T), o.NH₀ * o.p)
  elseif o.madjtype == :sidak  one(T) - (one(T) - o.p) ^ o.NH₀
  else o.p
  end
end

getb(o::StrBootTest) = isone(o.v_sd) ? o.numer[:,1] : o.numer[:,1] / o.v_sd  # numerator for full-sample test stat
getV(o::StrBootTest) = o.statDenom / ((isone(o.v_sd) ? o.smallsample : o.v_sd^2 * o.smallsample) * (o.sqrt ? o.multiplier^2 : o.multiplier) * o.dof)  # denominator for full-sample test stat
getv(o::StrBootTest) = @views isone(o.v_sd) ? o.v[:,2:end] : o.v[:,2:end] / o.v_sd  # wild weights
@inline getstat(o::StrBootTest) = o.multiplier * o.dist[1]


function relaxed_initial_state_TRI(mdp::NoCrashProblem, LastState::MLState, MKZAction::MLAction, DSRC_interval::Float64
                             ,rng=MersenneTwister(rand(UInt32)))

    pp = mdp.dmodel.phys_param
    #sim = HistoryRecorder(max_steps=steps, rng=rng)
    #policy = solve(solver, mdp)
    #policy = RandomPolicy(mdp)
    CurrentState = simulate_TRI(mdp, MKZAction,LastState, DSRC_interval,rng)
    #println("simulate done")
    return CurrentState
end

function simulate_TRI{S,A}(mdp::MDP{S,A}, MKZAction::MLAction,LastState::MLState, DSRC_interval::Float64,rng::AbstractRNG)
    sp = generate_sTRI(mdp, LastState, MKZAction, DSRC_interval,rng)
    return sp
end

function generate_sTRI(mdp::NoCrashProblem, s::MLState, a::MLAction, DSRC_interval::Float64, rng::AbstractRNG)

    @if_debug dbg_rng = copy(rng)

    sp::MLState=create_state(mdp)
    pp = mdp.dmodel.phys_param
    dt = pp.dt
    nb_cars = length(s.cars)
    resize!(sp.cars, nb_cars)
    #sp.x = 0
    sp.terminal = s.terminal

    ## Calculate deltas ##
    #====================#

    dxs = Array{Float64}(nb_cars)
    dvs = Array{Float64}(nb_cars)
    dys = Array{Float64}(nb_cars)
    lcs = Array{Float64}(nb_cars)

    # agent
    dvs[1] = a.acc*dt
    dxs[1] = s.cars[1].vel*dt + a.acc*dt^2/2.
    lcs[1] = a.lane_change
    dys[1] = a.lane_change*dt

    for i in 2:nb_cars
        neighborhood = get_neighborhood(pp, s, i)

        behavior = s.cars[i].behavior

        acc = gen_accel(behavior, mdp.dmodel, s, neighborhood, i, rng)
        dvs[i] = dt*acc
        dxs[i] = (s.cars[i].vel + dvs[i]/2.)*dt

        lcs[i] = gen_lane_change(behavior, mdp.dmodel, s, neighborhood, i, rng)
        dys[i] = lcs[i] * dt
    end

    ## Consistency checking ##
    #========================#

    # first prevent lane changes into each other
    changers = IntSet()
    for i in 1:nb_cars
        if lcs[i] != 0
            push!(changers, i)
        end
    end
    sorted_changers = sort!(collect(changers), by=i->s.cars[i].x, rev=true) # this might be slow because anonymous functions are slow
    # from front to back

    if length(sorted_changers) >= 2 #something to compare
        # iterate through pairs
        iter_state = start(sorted_changers)
        j, iter_state = next(sorted_changers, iter_state)
        while !done(sorted_changers, iter_state)
            i = j
            j, iter_state = next(sorted_changers, iter_state)
            car_i = s.cars[i] # front
            car_j = s.cars[j] # back

            # check if they are both starting to change lanes on this step
            if isinteger(car_i.y) && isinteger(car_j.y)

                # check if they are near each other lanewise
                if abs(car_i.y - car_j.y) <= 2.0

                    # make sure there is a conflict longitudinally
                    # if car_i.x - car_j.x <= pp.l_car || car_i.x + dxs[i] - car_j.x + dxs[j] <= pp.l_car
                    # if car_i.x - car_j.x <= mdp.dmodel.appear_clearance # made more conservative on 8/19
                    # if car_i.x - car_j.x <= get_idm_s_star(car_j.behavior.p_idm, car_j.vel, car_j.vel-car_i.vel) # upgraded to sstar on 8/19
                    ivp = car_i.vel + dt*dvs[i]
                    jvp = car_j.vel + dt*dvs[j]
                    ixp = car_i.x + dt*(car_i.vel + ivp)/2.0
                    jxp = car_j.x + dt*(car_j.vel + jvp)/2.0
                    n_max_acc_p = nullable_max_safe_acc(ixp-jxp-pp.l_car, jvp, ivp, pp.brake_limit,dt)
                    if ixp - jxp <= pp.l_car || car_i.x - car_j.x <= pp.l_car || isnull(n_max_acc_p) || get(n_max_acc_p) < -pp.brake_limit

                        # check if they are moving towards each other
                        # if dys[i]*dys[j] < 0.0 && abs(car_i.y+dys[i] - car_j.y+dys[j]) < 2.0
                        if true # prevent lockstepping 8/19 (doesn't prevent it that well)

                            # make j stay in his lane
                            dys[j] = 0.0
                            lcs[j] = 0.0
                        end
                    end
                end
            end
        end
    end

    # second, prevent cars hitting each other due to noise
    sorted = sort!(collect(1:length(s.cars)), by=i->s.cars[i].x, rev=true)

    if length(sorted) >= 2 #something to compare
        # iterate through pairs
        iter_state = start(sorted)
        j, iter_state = next(sorted, iter_state)
        while !done(sorted, iter_state)
            i = j
            j, iter_state = next(sorted, iter_state)
            if j == 1
                continue # don't check for the ego since the ego does not have noise
            end
            car_i = s.cars[i]
            car_j = s.cars[j]

            # check if they overlap longitudinally
            if car_j.x + dxs[j] > car_i.x + dxs[i] - pp.l_car

                # check if they will be in the same lane
                if occupation_overlap(car_i.y + dys[i], car_j.y + dys[j])
                    # warn and nudge behind
                    @if_debug begin
                        println("Conflict because of noise: front:$i, back:$j")
                        Gallium.@enter generate_s(mdp, s, a, dbg_rng)
                    end
                    if i == 1
                         warn("Car nudged because noise would cause a crash (ego in front).")
                        #error("Car nudged because noise would cause a crash (ego in front).")
                    else
                         warn("Car nudged because noise would cause a crash.")
                        #error("Car nudged because noise would cause a crash.")
                    end
                    dxs[j] = car_i.x + dxs[i] - car_j.x - 1.01*pp.l_car
                    dvs[j] = 2.0*(dxs[j]/dt - car_j.vel)
                end
            end
        end
    end

    ## Dynamics and Exits ##
    #======================#

    exits = IntSet()
    for i in 1:nb_cars
        car = s.cars[i]
        xp = car.x + (dxs[i] - dxs[1])
        yp = car.y + dys[i]
        # velp = max(min(car.vel + dvs[i],pp.v_max), pp.v_min)
        velp = max(car.vel + dvs[i], 0.0) # removed speed limits on 8/13
        # note lane change is updated above

        if dvs[i]/dt < -mdp.dmodel.brake_terminate_thresh
            sp.terminal = Nullable{Symbol}(:brake)
        end

        # check if a lane was crossed and snap back to it
        if isinteger(car.y)
            # prevent a multi-lane change in a single timestep
            if abs(yp-car.y) > 1.
                yp = car.y + sign(dys[i])
            end
        else # car.y is not an integer
            if floor(yp) >= ceil(car.y)
                yp = ceil(car.y)
            end
            if ceil(yp) <= floor(car.y)
                yp = floor(car.y)
            end
        end

        # if yp < 1.0 || yp > pp.nb_lanes
        #     @show i
        #     @show yp
        #     println("mdp = $mdp")
        #     println("s = $s")
        #     println("a = $a")
        # end
        #print("yp")
        #println(yp)
        @assert yp >= 1.0 && yp <= pp.nb_lanes

        if xp < 0.0 || xp >= pp.lane_length
            push!(exits, i)
        else
            sp.cars[i] = CarState(xp, yp, velp, lcs[i], car.behavior, s.cars[i].id)
        end
    end

    next_id = maximum([c.id for c in s.cars]) + 1

    deleteat!(sp.cars, exits)
    nb_cars -= length(exits)

    ## Generate new cars ##
    #=====================#

    if nb_cars < mdp.dmodel.nb_cars && rand(rng) <= mdp.dmodel.p_appear

        behavior = rand(rng, mdp.dmodel.behaviors)
        vel = typical_velocity(behavior) + randn(rng)*mdp.dmodel.vel_sigma

        clearances = Array{Float64}(pp.nb_lanes)
        fill!(clearances, Inf)
        closest_cars = Array{Int}(pp.nb_lanes)
        fill!(closest_cars, 0)
        sstar_margins = Array{Float64}(pp.nb_lanes)
        if vel > s.cars[1].vel
            # put at back
            # sstar is the sstar of the new guy
            for i in 1:length(s.cars)
                lowlane, highlane = occupation_lanes(s.cars[i].y, lcs[i])
                back = s.cars[i].x - pp.l_car
                if back < clearances[lowlane]
                    clearances[lowlane] = back
                    closest_cars[lowlane] = i
                end
                if back < clearances[highlane]
                    clearances[highlane] = back
                    closest_cars[highlane] = i
                end
            end
            for j in 1:pp.nb_lanes
                other = closest_cars[j]
                if other == 0
                    sstar = 0
                else
                    sstar = get_idm_s_star(behavior.p_idm, vel, vel-s.cars[other].vel)
                end
                sstar_margins[j] = clearances[j] - sstar
            end
        else
            for i in 1:length(s.cars)
                lowlane, highlane = occupation_lanes(s.cars[i].y, lcs[i])
                front = pp.lane_length - (s.cars[i].x + pp.l_car) # l_car is half the length of the old car plus half the length of the new one
                if front < clearances[lowlane]
                    clearances[lowlane] = front
                    closest_cars[lowlane] = i
                end
                if front < clearances[highlane]
                    clearances[highlane] = front
                    closest_cars[highlane] = i
                end
            end
            for j in 1:pp.nb_lanes
                other = closest_cars[j]
                if other == 0
                    sstar = 0
                else
                    sstar = get_idm_s_star(s.cars[other].behavior.p_idm,
                                           s.cars[other].vel,
                                           s.cars[other].vel-vel)
                end
                sstar_margins[j] = clearances[j] - sstar
            end
        end

        margin, lane = findmax(sstar_margins)

        if margin > 0.0
            if vel > s.cars[1].vel
                # at back
                push!(sp.cars, CarState(0.0, lane, vel, 0.0, behavior, next_id))
            else
                push!(sp.cars, CarState(pp.lane_length, lane, vel, 0.0, behavior, next_id))
            end
        end
    end

    if mdp.dmodel.lane_terminate && sp.cars[1].y == mdp.rmodel.target_lane
        sp.terminal = Nullable{Any}(:lane)
    elseif sp.x > mdp.dmodel.max_dist
        sp.terminal = Nullable{Any}(:distance)
    end

    # sp.crashed = is_crash(mdp, s, sp, warning=false)
    # sp.crashed = false

    @assert sp.cars[1].x == s.cars[1].x # ego should not move
    sp.x = s.x + sp.cars[1].vel*0.75
    sp.t = s.t
    ca = control_action(mdp,s)
    if s.Control_Signal && (a == ca)
       sp.Control_Signal = true
    else
       sp.Control_Signal = false
    end

    if s.x > 2500 && isinteger(s.cars[1].y) && s.t == 0.0
        sp.Control_Signal = true
        sp.t = 1.0
    end

    #if s.cars[1].y == 4.0 && s.t == 0.0
    #    sp.Control_Signal = true
    #    sp.t = 1.0
    #end
    return sp
end


function parse_cmdline()
    s = ArgParseSettings()

    @add_arg_table! s begin
        "mcmc"
            help = "Run MCMC to optimize hyperparameters"
            action = :command
        "predict"
            help = "Calculate the posterior of a GP at tdata given data"
            action = :command
        "sample"
            help = "Sample the posterior of a GP"
            action = :command
        "fitplot"
            help = "Diagnostic plots showing the posteriors of different components of the GP"
            action = :command
        "select"
            help = "Output model selection parameters; requires --mcmc and --mcmc2"
            action = :command
        "--log"
            help = "Direct logging output to a file"
        "--debug"
            help = "Enable debug logging"
            action = :store_true
    end

    @add_arg_table! s["mcmc"] begin
        "formula"
            help = "Gaussian Process formula"
            required = true
        "--data", "-x"
            help = "Input data files, accepts \"stdin\". ;-separated, use : to provide additional flags which can be combined: \"#:\" transposes the table, \",:\" reads as CSV, \"~:\" reads as TSV (default). Other characters before : give the column/row to join the tables with, e.g. id:data.tsv;#subjectid:subjects.tsv will use the id column of data.tsv and subjectid row of subjects.tsv."
            required = true
        "--rmv_outliers"
            help = "Outlier removal method for training data (none|fence)"
            default = "none"
        "--outlier_fields"
            help = ";-separated list of additional fields to include in outlier removal"
            default = ""
        "--outlier_ignore"
            help = ";-separated list of fields to ignore for outlier removal"
            default = ""
        "--mcmc", "-m"
            help = "MCMC samples for hyperparameters; if provided, the chain will be extended. Does NOT support --data format flags"
        "--burnin", "-r"
            help = "Burn-in samples"
            arg_type = Int
            default = 0
        "--thin", "-t"
            help = "Thinning"
            arg_type = Int
            default = 1
        "--samples", "-n"
            help = "Number of MCMC samples to generate (after anneal, burn-in, and thinning)"
            arg_type = Int
            default = 100
        "--output", "-o"
            help = "Output filename, accepts \"stdout\", does NOT support --data format flags"
            default = "stdout"
    end

    @add_arg_table! s["predict"] begin
        "formula"
            help = "Gaussian Process formula"
            required = true
        "--data", "-x"
            help = "Input data files, accepts \"stdin\". ;-separated, use : to provide additional flags which can be combined: \"#:\" transposes the table, \",:\" reads as CSV, \"~:\" reads as TSV (default). Other characters before : give the column/row to join the tables with, e.g. id:data.tsv;#subjectid:subjects.tsv will use the id column of data.tsv and subjectid row of subjects.tsv."
            required = true
        "--rmv_outliers"
            help = "Outlier removal method for training data (none|fence)"
            default = "none"
        "--outlier_fields"
            help = ";-separated list of additional fields to include in outlier removal"
            default = ""
        "--outlier_ignore"
            help = ";-separated list of fields to ignore for outlier removal"
            default = ""
        "--mcmc", "-m"
            help = "MCMC samples for hyperparameters, does NOT support --data format flags"
        "--atdata", "-t"
            help = "Data files providing variable values at which to make predictions"
        "--at"
            help = "Predict at these points (alternative to tdata); format: ;-separated variable=start:step:end OR variable=value OR variable=Julia distribution OR variable/group=value"
        "--output", "-o"
            help = "Output filename, accepts \"stdout\", supports --data format flags"
            default = "stdout"
    end

    @add_arg_table! s["sample"] begin
        "formula"
            help = "Gaussian Process formula"
            required = true
        "--data", "-x"
            help = "Input data files, accepts \"stdin\". ;-separated, use : to provide additional flags which can be combined: \"#:\" transposes the table, \",:\" reads as CSV, \"~:\" reads as TSV (default). Other characters before : give the column/row to join the tables with, e.g. id:data.tsv;#subjectid:subjects.tsv will use the id column of data.tsv and subjectid row of subjects.tsv."
        "--rmv_outliers"
            help = "Outlier removal method for training data (none|fence)"
            default = "none"
        "--outlier_fields"
            help = ";-separated list of additional fields to include in outlier removal"
            default = ""
        "--outlier_ignore"
            help = ";-separated list of fields to ignore for outlier removal"
            default = ""
        "--mcmc", "-m"
            help = "MCMC samples for hyperparameters, does NOT support --data format flags"
        "--atdata", "-t"
            help = "Data files providing variable values at which to sample the GP"
        "--at"
            help = "Predict at these points (alternative to tdata); format: ;-separated variable=start:step:end OR variable=value OR variable=Julia distribution OR variable/group=value"
        "--output", "-o"
            help = "Output filename, accepts \"stdout\", supports --data format flags"
            default = "stdout"
        "--plot"
            help = "Output filename for the sample plots"
        "--plotx"
            help = "X axis of the sample plot; can also be \"x:group\" to provide a grouping variable as well"
    end

    @add_arg_table! s["fitplot"] begin
        "formula"
            help = "Gaussian Process formula"
            required = true
        "--data", "-x"
            help = "Input data files, accepts \"stdin\". ;-separated, use : to provide additional flags which can be combined: \"#:\" transposes the table, \",:\" reads as CSV, \"~:\" reads as TSV (default). Other characters before : give the column/row to join the tables with, e.g. id:data.tsv;#subjectid:subjects.tsv will use the id column of data.tsv and subjectid row of subjects.tsv."
            required = true
        "--rmv_outliers"
            help = "Outlier removal method for training data (none|fence)"
            default = "none"
        "--outlier_fields"
            help = ";-separated list of additional fields to include in outlier removal"
            default = ""
        "--outlier_ignore"
            help = ";-separated list of fields to ignore for outlier removal"
            default = ""
        "--mcmc", "-m"
            help = "MCMC samples for hyperparameters, does NOT support --data format flags"
        "--component", "--comp"
            help = "Plot fit plots for components with the given variables"
        "--output", "-o"
            help = "Output filename"
            default = "fitplots.pdf"
    end

    @add_arg_table! s["select"] begin
        "--mcmc", "-m"
            help = "MCMC samples for hyperparameters, does NOT support --data format flags"
        "--mcmc2", "-c"
            help = "MCMC samples for hyperparameters of the second model (for model comparison), does NOT support --data format flags"
    end

    return parse_args(s)
end


include("utils.jl") # contains most dependencies and the saving function
using GenerativeAD.Models: anomaly_score, generalized_anomaly_score_gpu

s = ArgParseSettings()
@add_arg_table! s begin
	"dataset"
		default = "MNIST"
		arg_type = String
		help = "dataset"
	"class"
		default = 1
		arg_type = Int
		help = "dataset"
end
parsed_args = parse_args(ARGS, s)
@unpack dataset, class = parsed_args

#######################################################################################
################ THIS PART IS TO BE PROVIDED FOR EACH MODEL SEPARATELY ################
modelname = "Conv-SkipGANomaly"

"""
This returns encodings, parameters and scoring functions in order to reconstruct the experiment.
This is a slightly updated version of the original run script.
"""
function evaluate(model_data, data, parameters)
	# load the model file, extract params and model
	model = model_data["model"] |> gpu

	# construct return information - put e.g. the model structure here for generative models
	training_info = (
		fit_t = get(model_data, "fit_t", nothing),
		history = get(model_data, "history", nothing),
		iters = get(model_data, "iters", nothing),
		npars = get(model_data, "npars", nothing),
		model = model |> cpu
		)

	return training_info, [(x -> generalized_anomaly_score_gpu(model|>cpu, x, R=r, L=l, lambda=lam),
		merge(parameters, (R=r, L=l, test_lambda=lam,)))
		for r in ["mae", "mse"] for l in ["mae", "mse"] for lam = 0.1:0.1:0.9 ]
end

##################
# this is common #
##################
seed = 1
method = "leave-one-in"
contamination = 0.0
ac = class

main_inpath = datadir("experiments/images_leave-one-in_backup/$(modelname)/$(dataset)")
main_savepath = datadir("experiments/images_leave-one-in/$(modelname)/$(dataset)")
mkpath(main_savepath)

# this loop unfortunately cannot be in a function, since loading of bson is only safe ot top level
global data = GenerativeAD.load_data(dataset, seed=seed, anomaly_class_ind=ac, method=method,
	contamination=contamination)
in_ch = size(data[1][1],3)
isize = maximum([size(data[1][1],1),size(data[1][1],2)])
isize = (isize % 16 != 0) ? isize + 16 - isize % 16 : isize

inpath = joinpath(main_inpath, "ac=$ac/seed=$seed")
savepath = joinpath(main_savepath, "ac=$ac/seed=$seed")
mkpath(savepath)
fs = readdir(inpath, join=true)
sfs = filter(x->!(occursin("model", x)), fs)
mfs = filter(x->(occursin("model", x)), fs)

@info "Loaded $(length(mfs)) modelfiles in $inpath, processing..."
for mf in mfs
	# load the bson file on top level, otherwise we get world age problems
	model_data = load(mf)
	if haskey(model_data, "parameters")
		parameters = model_data["parameters"]
	else # this is in case parameters are not saved in the model file
		init_seed = DrWatson.parse_savename(mf)[2]["init_seed"]
		sf = sfs[map(x->DrWatson.parse_savename(x)[2]["init_seed"], sfs) .== init_seed][1]
		score_data = load(sf)
		parameters = score_data[:parameters]
	end
	parameters = merge(parameters, (isize=isize, in_ch = in_ch, out_ch = 1))
	# update parameter
	global data = GenerativeAD.Models.preprocess_images(data, parameters)

	try
		training_info, results = evaluate(model_data, data, parameters) # this produces parameters, encodings, score funs
		save_results(parameters, training_info, results, savepath, data,
			ac, modelname, seed, dataset, contamination) # this computes and saves score and model files
	catch e
		if isa(e, LoadError)
			@warn "$mf failed during result evaluation due to $e"
		else
			rethrow(e)
		end
	end
end


mutable struct GFFixUpdateState
    prev_trace::GFTrace
    trace::GFTrace
    constraints::Any
    score::Float64
    weight::Float64
    visitor::AddressVisitor
    params::Dict{Symbol,Any}
    discard::DynamicAssignment
    args_change::Any
    retchange::Union{Some{Any},Nothing}
    callee_output_changes::HomogenousTrie{Any,Any}
end

function GFFixUpdateState(args_change, prev_trace, constraints, params)
    visitor = AddressVisitor()
    discard = DynamicAssignment()
    GFFixUpdateState(prev_trace, GFTrace(), constraints, 0., 0., visitor,
                     params, discard, args_change, nothing,
                     HomogenousTrie{Any,Any}())
end

get_args_change(state::GFFixUpdateState) = state.args_change

function set_ret_change!(state::GFFixUpdateState, value)
    if state.retchange === nothing
        state.retchange = value
    else
        lightweight_retchange_already_set_err()
    end
end

function get_addr_change(state::GFFixUpdateState, addr)
    get_leaf_node(state.callee_output_changes, addr)
end

function addr(state::GFFixUpdateState, dist::Distribution{T}, args, addr) where {T}
    visit!(state.visitor, addr)
    constrained = has_leaf_node(state.constraints, addr)
    has_previous = has_primitive_call(state.prev_trace, addr)
    if has_internal_node(state.constraints, addr)
        lightweight_got_internal_node_err(addr)
    end
    if constrained && !has_previous
        error("fix_update constrain a new address: $addr")
    end
    local retval::T
    if has_previous
        prev_call::CallRecord = get_primitive_call(state.prev_trace, addr)
        prev_retval::T = prev_call.retval
        if constrained
            retval = get_leaf_node(state.constraints, addr)
            set_leaf_node!(state.discard, addr, prev_retval)
        else
            retval = prev_retval
        end
    else
        retval = random(dist, args...)
    end
    score = logpdf(dist, retval, args...)
    call = CallRecord(score, retval, args)
    state.trace = assoc_primitive_call(state.trace, addr, call)
    state.score += score
    if has_previous
        state.weight += score - prev_call.score
    end
    retval
end

function addr(state::GFFixUpdateState, gen::Generator{T}, args, addr, args_change) where {T}
    visit!(state.visitor, addr)
    if has_internal_node(state.constraints, addr)
        constraints = get_internal_node(state.constraints, addr)
    elseif has_leaf_node(state.constraints, addr)
        lightweight_got_leaf_node_err(addr)
    else
        constraints = EmptyAssignment()
    end
    local retval::T
    local trace::GFTrace
    if has_subtrace(state.prev_trace, addr)
        prev_trace = get_subtrace(state.prev_trace, addr)
        (trace, weight, discard, retchange) = fix_update(gen, args, args_change, prev_trace, constraints)
        state.weight += weight
        set_internal_node!(state.discard, addr, discard)
        set_leaf_node!(state.callee_output_changes, addr, retchange)
    else
        trace = simulate(gen, args)
        set_leaf_node!(state.callee_output_changes, addr, nothing)
    end
    call::CallRecord = get_call_record(trace)
    retval = call.retval
    state.trace = assoc_subtrace(state.trace, addr, trace)
    state.score += call.score
    retval
end

function splice(state::GFFixUpdateState, gen::GenFunction, args::Tuple)
    exec(gf, state, args)
end

function fix_update(gf::GenFunction, args, args_change, prev_trace::GFTrace, constraints)
    state = GFFixUpdateState(args_change, prev_trace, constraints, gf.params)
    retval = exec(gf, state, args)
    new_call = CallRecord(state.score, retval, args)
    state.trace.call = new_call
    unconsumed = get_unvisited(state.visitor, constraints)
    if !isempty(unconsumed)
        error("Update did not consume all constraints")
    end
    (state.trace, state.weight, state.discard, state.retchange)
end


##### Multi-physics types

using CLIMAParameters.Planet: T_freeze, cv_l

using ..Microphysics_0M
using ..Microphysics

export Subsidence
struct Subsidence{PV, FT} <: TendencyDef{Source, PV}
    D::FT
end

# Subsidence includes tendencies in Mass, Energy and TotalMoisture equations:
Subsidence(D::FT) where {FT} = (
    Subsidence{Mass, FT}(D),
    Subsidence{Energy, FT}(D),
    Subsidence{TotalMoisture, FT}(D),
)

subsidence_velocity(subsidence::Subsidence{PV, FT}, z::FT) where {PV, FT} =
    -subsidence.D * z

struct PressureGradient{PV <: Momentum} <: TendencyDef{Flux{FirstOrder}, PV} end
struct Pressure{PV <: Energy} <: TendencyDef{Flux{FirstOrder}, PV} end

struct Advect{PV} <: TendencyDef{Flux{FirstOrder}, PV} end
struct Diffusion{PV} <: TendencyDef{Flux{SecondOrder}, PV} end

export RemovePrecipitation
"""
    RemovePrecipitation{PV} <: TendencyDef{Source, PV}
A sink to `q_tot` when cloud condensate is exceeding a threshold.
The threshold is defined either in terms of condensate or supersaturation.
The removal rate is implemented as a relaxation term
in the Microphysics_0M module.
The default thresholds and timescale are defined in CLIMAParameters.jl.
"""
struct RemovePrecipitation{PV <: Union{Mass, Energy, TotalMoisture}} <:
       TendencyDef{Source, PV}
    " Set to true if using qc based threshold"
    use_qc_thr::Bool
end

RemovePrecipitation(b::Bool) = (
    RemovePrecipitation{Mass}(b),
    RemovePrecipitation{Energy}(b),
    RemovePrecipitation{TotalMoisture}(b),
)

function remove_precipitation_sources(
    s::RemovePrecipitation{PV},
    atmos,
    args,
) where {PV <: Union{Mass, Energy, TotalMoisture}}
    @unpack state, aux = args
    @unpack ts = args.precomputed

    FT = eltype(state)

    q = PhasePartition(ts)
    λ::FT = liquid_fraction(ts)
    I_l::FT = internal_energy_liquid(ts)
    I_i::FT = internal_energy_ice(ts)
    Φ::FT = gravitational_potential(atmos.orientation, aux)

    S_qt::FT = 0
    if s.use_qc_thr
        S_qt = remove_precipitation(atmos.param_set, q)
    else
        q_vap_sat = q_vap_saturation(ts)
        S_qt = remove_precipitation(atmos.param_set, q, q_vap_sat)
    end

    S_e::FT = (λ * I_l + (1 - λ) * I_i + Φ) * S_qt

    return (S_ρ_qt = state.ρ * S_qt, S_ρ_e = state.ρ * S_e)
end

export WarmRain_1M
"""
    WarmRain_1M{FT} <: AbstractSource
A collection of source/sink terms related to 1-moment warm rain microphysics.
"""
struct WarmRain_1M{
    PV <: Union{Mass, Energy, TotalMoisture, LiquidMoisture, Rain},
} <: TendencyDef{Source, PV} end

WarmRain_1M() = (
    WarmRain_1M{Mass}(),
    WarmRain_1M{Energy}(),
    WarmRain_1M{TotalMoisture}(),
    WarmRain_1M{LiquidMoisture}(),
    WarmRain_1M{Rain}(),
)

function warm_rain_sources(atmos, args)
    @unpack state, aux = args
    @unpack ts = args.precomputed

    FT = eltype(state)

    q = PhasePartition(ts)
    T::FT = air_temperature(ts)
    I_l::FT = internal_energy_liquid(ts)
    q_rai::FT = state.precipitation.ρq_rai / state.ρ
    Φ::FT = gravitational_potential(atmos.orientation, aux)

    # autoconversion
    src_q_rai_acnv = conv_q_liq_to_q_rai(atmos.param_set.microphys.rai, q.liq)
    # accretion
    src_q_rai_accr = accretion(
        atmos.param_set,
        atmos.param_set.microphys.liq,
        atmos.param_set.microphys.rai,
        q.liq,
        q_rai,
        state.ρ,
    )
    # rain evaporation
    src_q_rai_evap = evaporation_sublimation(
        atmos.param_set,
        atmos.param_set.microphys.rai,
        q,
        q_rai,
        state.ρ,
        T,
    )

    S_qr::FT = src_q_rai_acnv + src_q_rai_accr + src_q_rai_evap
    S_ql::FT = -src_q_rai_acnv - src_q_rai_accr
    S_qt::FT = -S_qr
    S_e::FT = S_qt * (I_l + Φ)

    return (
        S_ρ_qt = state.ρ * S_qt,
        S_ρ_ql = state.ρ * S_ql,
        S_ρ_qr = state.ρ * S_qr,
        S_ρ_e = state.ρ * S_e,
    )
end

export RainSnow_1M
"""
    RainSnow_1M{FT} <: AbstractSource
A collection of source/sink terms related to 1-moment rain and snow microphysics
"""
struct RainSnow_1M{PV <: Union{Mass, Energy, Moisture, Rain, Snow}} <:
       TendencyDef{Source, PV} end

RainSnow_1M() = (
    RainSnow_1M{Mass}(),
    RainSnow_1M{Energy}(),
    RainSnow_1M{TotalMoisture}(),
    RainSnow_1M{LiquidMoisture}(),
    RainSnow_1M{IceMoisture}(),
    RainSnow_1M{Rain}(),
    RainSnow_1M{Snow}(),
)

function rain_snow_sources(atmos, args)
    @unpack state, aux = args
    @unpack ts = args.precomputed

    FT = eltype(state)

    q_rai::FT = state.precipitation.ρq_rai / state.ρ
    q_sno::FT = state.precipitation.ρq_sno / state.ρ
    q = PhasePartition(ts)
    T::FT = air_temperature(ts)
    I_d::FT = internal_energy_dry(ts)
    I_v::FT = internal_energy_vapor(ts)
    I_l::FT = internal_energy_liquid(ts)
    I_i::FT = internal_energy_ice(ts)
    _T_freeze::FT = T_freeze(atmos.param_set)
    _L_f::FT = latent_heat_fusion(ts)
    _cv_l::FT = cv_l(atmos.param_set)
    Φ::FT = gravitational_potential(atmos.orientation, aux)

    # temporary vars for summming different source terms
    S_qr::FT = FT(0)
    S_qs::FT = FT(0)
    S_ql::FT = FT(0)
    S_qi::FT = FT(0)
    S_qt::FT = FT(0)
    S_e::FT = FT(0)

    # source of rain via autoconversion
    tmp = conv_q_liq_to_q_rai(atmos.param_set.microphys.rai, q.liq)
    S_qr += tmp
    S_ql -= tmp
    S_e -= tmp * (I_l + Φ)

    # source of snow via autoconversion
    tmp = conv_q_ice_to_q_sno(
        atmos.param_set,
        atmos.param_set.microphys.ice,
        q,
        state.ρ,
        T,
    )
    S_qs += tmp
    S_qi -= tmp
    S_e -= tmp * (I_i + Φ)

    # source of rain water via accretion cloud water - rain
    tmp = accretion(
        atmos.param_set,
        atmos.param_set.microphys.liq,
        atmos.param_set.microphys.rai,
        q.liq,
        q_rai,
        state.ρ,
    )
    S_qr += tmp
    S_ql -= tmp
    S_e -= tmp * (I_l + Φ)

    # source of snow via accretion cloud ice - snow
    tmp = accretion(
        atmos.param_set,
        atmos.param_set.microphys.ice,
        atmos.param_set.microphys.sno,
        q.ice,
        q_sno,
        state.ρ,
    )
    S_qs += tmp
    S_qi -= tmp
    S_e -= tmp * (I_i + Φ)

    # sink of cloud water via accretion cloud water - snow
    tmp = accretion(
        atmos.param_set,
        atmos.param_set.microphys.liq,
        atmos.param_set.microphys.sno,
        q.liq,
        q_sno,
        state.ρ,
    )
    if T < _T_freeze # cloud droplets freeze to become snow)
        S_qs += tmp
        S_ql -= tmp
        S_e -= tmp * (I_i + Φ)
    else # snow melts, both cloud water and snow become rain
        α::FT = _cv_l / _L_f * (T - _T_freeze)
        S_ql -= tmp
        S_qs -= tmp * α
        S_qr += tmp * (1 + α)
        S_e -= tmp * ((1 + α) * I_l - α * I_i + Φ)
    end

    # sink of cloud ice via accretion cloud ice - rain
    tmp1 = accretion(
        atmos.param_set,
        atmos.param_set.microphys.ice,
        atmos.param_set.microphys.rai,
        q.ice,
        q_rai,
        state.ρ,
    )
    # sink of rain via accretion cloud ice - rain
    tmp2 = accretion_rain_sink(
        atmos.param_set,
        atmos.param_set.microphys.ice,
        atmos.param_set.microphys.rai,
        q.ice,
        q_rai,
        state.ρ,
    )
    S_qi -= tmp1
    S_e -= tmp1 * (I_i + Φ)
    S_qr -= tmp2
    S_e += tmp2 * _L_f
    S_qs += tmp1 + tmp2

    # accretion rain - snow
    if T < _T_freeze
        tmp = accretion_snow_rain(
            atmos.param_set,
            atmos.param_set.microphys.sno,
            atmos.param_set.microphys.rai,
            q_sno,
            q_rai,
            state.ρ,
        )
        S_qs += tmp
        S_qr -= tmp
        S_e += tmp * _L_f
    else
        tmp = accretion_snow_rain(
            atmos.param_set,
            atmos.param_set.microphys.rai,
            atmos.param_set.microphys.sno,
            q_rai,
            q_sno,
            state.ρ,
        )
        S_qs -= tmp
        S_qr += tmp
        S_e -= tmp * _L_f
    end

    # rain evaporation sink (it already has negative sign for evaporation)
    tmp = evaporation_sublimation(
        atmos.param_set,
        atmos.param_set.microphys.rai,
        q,
        q_rai,
        state.ρ,
        T,
    )
    S_qr += tmp
    S_e -= tmp * (I_l + Φ)

    # snow sublimation/deposition source/sink
    tmp = evaporation_sublimation(
        atmos.param_set,
        atmos.param_set.microphys.sno,
        q,
        q_sno,
        state.ρ,
        T,
    )
    S_qs += tmp
    S_e -= tmp * (I_i + Φ)

    # snow melt
    tmp = snow_melt(
        atmos.param_set,
        atmos.param_set.microphys.sno,
        q_sno,
        state.ρ,
        T,
    )
    S_qs -= tmp
    S_qr += tmp
    S_e -= tmp * _L_f

    # total qt sink is the sum of precip sources
    S_qt = -S_qr - S_qs

    return (
        S_ρ_qt = state.ρ * S_qt,
        S_ρ_ql = state.ρ * S_ql,
        S_ρ_qi = state.ρ * S_qi,
        S_ρ_qr = state.ρ * S_qr,
        S_ρ_qs = state.ρ * S_qs,
        S_ρ_e = state.ρ * S_e,
    )
end


module CommunalHelperCustomSummitGemManager

using ..Ahorn, Maple

@mapdef Entity "CommunalHelper/CustomSummitGemManager" SummitGemManager(
    x::Integer,
    y::Integer,
    gemIds::String="",
    melody::String="",
    heartOffset::String="0.0,0.0",
    nodes::Array{Tuple{Integer,Integer},1}=Tuple{Integer,Integer}[],
)

const placements = Ahorn.PlacementDict(
    "Summit Gem Manager (Communal Helper)" => Ahorn.EntityPlacement(
        SummitGemManager,
    ),
)

Ahorn.nodeLimits(entity::SummitGemManager) = 0, -1

const gemSprite = "collectables/summitgems/0/gem00"

const smallGemSprites = String["collectables/summitgems/$i/small00" for i in 0:7]

function Ahorn.selection(entity::SummitGemManager)
    x, y = Ahorn.position(entity)

    nodes = get(entity.data, "nodes", ())

    res = Ahorn.Rectangle[Ahorn.Rectangle(x - 15, y - 15, 30, 30)]

    for node in nodes
        nx, ny = node
        push!(res, Ahorn.getSpriteRectangle(gemSprite, nx, ny))
    end

    hx, hy = getHeartOffset(entity)
    if hx + hy != 0
        push!(res, Ahorn.getSpriteRectangle("collectables/heartGem/ghost00", x + hx, y + hy))
    end

    return res
end

# Cruor please don't yell at me
function Ahorn.Selection.applyMovement!(target::SummitGemManager, ox, oy, node=0)
    if node == 0
        target.data["x"] += ox
        target.data["y"] += oy
    else
        nodes = get(target.data, "nodes", ())

        if length(nodes) >= node
            nodes[node] = nodes[node] .+ (ox, oy)
        elseif node == length(nodes) + 1
            heartOffset = getHeartOffset(target)
            (hx, hy) = heartOffset .+ (ox, oy)
            target.data["heartOffset"] = string(hx, ",", hy)
        end
    end

    return true
end

# not currently working
function Ahorn.deleted(target::SummitGemManager, node::Int64)
    nodes = get(target.data, "nodes", ())

    if node == length(nodes) + 1
        target.data["heartOffset"] = "0.0,0.0"
    end
end

function getHeartOffset(entity::SummitGemManager)
    heartOffset = get(entity.data, "heartOffset", "")
    if length(heartOffset) > 3
        heartOffset = (split(heartOffset, ","))
        heartOffset = tryparse.(Float64, heartOffset)
        if all(f -> isnothing(f), heartOffset)
            return (0, 0)
        end
        return (heartOffset[1], heartOffset[2])
    end
    return (0, 0)
end

function Ahorn.renderSelectedAbs(ctx::Ahorn.Cairo.CairoContext, entity::SummitGemManager)
    x, y = Ahorn.position(entity)

    nodes = get(entity.data, "nodes", ())

    for node in nodes
        nx, ny = node

        Ahorn.drawLines(ctx, Tuple{Number, Number}[(x, y), (nx, ny)], Ahorn.colors.selection_selected_fc)
    end

    hx, hy = getHeartOffset(entity)
    if hx + hy != 0
        Ahorn.drawArrow(ctx, x, y, x + hx, y + hy, Ahorn.colors.selection_selected_fc; headLength=6, headTheta=pi/8)
    end
end

function Ahorn.renderAbs(ctx::Ahorn.Cairo.CairoContext, entity::SummitGemManager)
    x, y = Ahorn.position(entity)

    hx, hy = getHeartOffset(entity)
    if hx + hy != 0
        Ahorn.drawSprite(ctx, "collectables/heartGem/ghost00", x + hx, y + hy)
    end

    Ahorn.Cairo.save(ctx)
    Ahorn.set_antialias(ctx, 1)
    Ahorn.set_line_width(ctx, 1)
    Ahorn.Cairo.arc(ctx, x, y, 20, 0, pi * 2)
    Ahorn.Cairo.set_source_rgba(ctx, 0.0, 0.0, 0.0, 0.4)
    Ahorn.Cairo.fill_preserve(ctx)
    Ahorn.Cairo.set_source_rgba(ctx, 1.0, 1.0, 1.0, 1.0)
    Ahorn.Cairo.stroke(ctx)
    Ahorn.restore(ctx)

    for i in 1:length(smallGemSprites)
        gx, gy = Base.Math.cospi((i / length(smallGemSprites)) * 2) * 14,
        Base.Math.sinpi((i / length(smallGemSprites)) * 2) * 14
        Ahorn.drawSprite(ctx, smallGemSprites[i], x + gx, y + gy)
    end

    nodes = get(entity.data, "nodes", ())

    for node in nodes
        nx, ny = node
        Ahorn.drawSprite(ctx, gemSprite, nx, ny)
    end
end

end


function go_front(tar::Entity)
	l=steplength(tar)
	move!(tar,l*sin(tar.θ),-l*cos(tar.θ))
end
function go_back(tar::Entity)
	l=steplength(tar)
	move!(tar,-l*sin(tar.θ),l*cos(tar.θ))
end
go_north(tar::Entity)=move!(tar,0,-steplength(tar))
go_south(tar::Entity)=move!(tar,0,steplength(tar))
go_west(tar::Entity)=move!(tar,-steplength(tar),0)
go_east(tar::Entity)=move!(tar,steplength(tar),0)
turn_left(tar::Entity)=tar.θ-=π/12
turn_right(tar::Entity)=tar.θ+=π/12
"pos是已经过处理的地图坐标"
function mousedown(pos::Pair{Float,Float})
	bx=floor(pos.first)
	by=floor(pos.second)
	if !c_reach(ply,bx,by) return end
	plyclickblk(bx,by)
end
function plyclickblk(x::Int,y::Int,p::玩家=ply)
	blk=getblk(x,y)
	if isa(blk,气体)||isa(blk,液体)
		plyfill(x,y)
		return
	end
	tm=destroy_with(p.ib[p.chosen],blk)
	if (!lsetting[:break_all]) && tm==-1
		info_help(:game,:not_destroyable)
		return
	end
	if tm<4
		plydestroy(x,y)
		return
	end
	# 摧毁进程
	cou=0
	mouse_up=Channel{Nothing}(1)
	id=signal_connect(dcon,:mouse_release_event) do widget,event
		put!(mouse_up,nothing)
	end
	sur=CairoImageSurface(loadedimgs[:dig0])
	after_show[:d]=()->begin
		pos=get_show_pos(x,y)
		move_to(dcon,pos.first,pos.second)
		set_source_surface(dcon,sur)
	end
	t=@task begin
		while true
			sleep(0.5)
			cou+=1
			if !isempty(mouse_up)
				if cou>tm plydestroy(x,y)
				else info_help(:game,:not_enough_time)
				end
				break
			elseif cou>tm wait(mouse_up)
			elseif cou>tm>>2 sur=CairoImageSurface(loadedimgs[:dig1])
			elseif cou>tm>>1 sur=CairoImageSurface(loadedimgs[:dig2])
			end
		end
	end
	schedule(t)
	delete!(after_show,:d)
	signal_handler_disconnect(dcon,id)
end
function plyfill(x::Int,y::Int,p::玩家=ply)
	it=p.ib.i[p.chosen]
	blk=t_blk(it)
	if blk==missing return false
	else
		setblk(x,y,blk;player_put=true)
		if !lsetting[:inf_itm]
			reduce!(p.ib,p.chosen,1)
			updg(p)
		end
		return true
	end
end
function plydestroy(x::Int,y::Int,p::玩家=ply)
	setblk(x,y,空气())
end

function env_light()
#=
0.05s一帧，一昼夜32768帧，即27min 18.4s
round(sin(t/16384*π)*8+7.5)
=#
	t=UInt16(lsetting[:t]&32767)
	te=(t>16384 ? t-32768 : t)>>1
	res= te<327||te>7865 ? 0x0 :
	te<659||te>7533 ? 0x1 :
	te<1003||te>7189 ? 0x2 :
	te<1366||te>6826 ? 0x3 :
	te<1761||te>6431 ? 0x4 :
	te<2212||te>5980 ? 0x5 :
	te<2779||te>5413 ? 0x6 : 0x7
    return (t>16384) ? 0x7-res : res+0x8
end