#################################################################################
# Comments
#
# - Ideally the net_injection variables would be bounded.  This can be done using an adhoc data model extention
# - the `instantiate_*_expr_model` functions combine `PM.instantiate_model` and the `build_*` methods
#################################################################################
# Model Definitions

""
function instantiate_nip_expr_model(data::Dict{String, Any}, model_constructor; kwargs...)
    return PM.instantiate_model(data, model_constructor, instantiate_nip_expr; kwargs...)
end

""
# replicates PM.build_mn_opf
function instantiate_nip_expr(pm::PM.AbstractPowerModel)
    for n in eachindex(PM.nws(pm))
        @assert !PM.ismulticonductor(pm, nw = n)
        PM.variable_bus_voltage(pm, nw = n)
        PM.variable_branch_power(pm, nw = n; bounded = false)
        PM.variable_dcline_power(pm, nw = n)

        PM.constraint_model_voltage(pm, nw = n)

        for i in PM.ids(pm, :ref_buses, nw = n)
            PM.constraint_theta_ref(pm, i, nw = n)
        end

        for i in PM.ids(pm, :bus, nw = n)
            constraint_power_balance_ni_expr(pm, i, nw = n)
        end

        for i in PM.ids(pm, :branch, nw = n)
            PM.constraint_ohms_yt_from(pm, i, nw = n)
            PM.constraint_ohms_yt_to(pm, i, nw = n)

            PM.constraint_voltage_angle_difference(pm, i, nw = n)
        end

        for i in PM.ids(pm, :dcline, nw = n)
            PM.constraint_dcline_power_losses(pm, i, nw = n)
        end
    end

    return
end

""
function instantiate_nip_ptdf_expr_model(
    data::Dict{String, Any},
    model_constructor;
    kwargs...,
)
    return PM.instantiate_model(
        data,
        PM.DCPPowerModel,
        instantiate_nip_ptdf_expr;
        ref_extensions = [PM.ref_add_connected_components!, PM.ref_add_sm!],
        kwargs...,
    )
end

""
# replicates PM.build_opf_ptdf
function instantiate_nip_ptdf_expr(pm::PM.AbstractPowerModel)
    for n in eachindex(PM.nws(pm))
        @assert !PM.ismulticonductor(pm, nw = n)

        #PM.variable_gen_power(pm) #connect P__* with these

        for i in PM.ids(pm, :bus, nw = n)
            if !haskey(PM.var(pm, n), :inj_p)
                PM.var(pm, n)[:inj_p] = Dict{Int, Any}()
            end
            PM.var(pm, n, :inj_p)[i] = PM.ref(pm, :bus, i, nw = n)["inj_p"] # use :nodal_balance_expr
        end

        PM.constraint_model_voltage(pm, nw = n)

        # this constraint is implicit in this model
        #for i in PM.ids(pm, :ref_buses, nw = n)
        #    PM.constraint_theta_ref(pm, i, nw = n)
        #end

        # done later with PSI copper_plate
        #for i in PM.ids(pm, :bus, nw = n)
        #    constraint_power_balance_ni_expr(pm, i, nw = n)
        #end
        for (i, branch) in PM.ref(pm, :branch, nw = n)
            if haskey(branch, "rate_a")
                PM.expression_branch_power_ohms_yt_from_ptdf(pm, i, nw = n)
                PM.expression_branch_power_ohms_yt_to_ptdf(pm, i, nw = n)
            end

            # done in PSI construct_branch!
            #PM.constraint_thermal_limit_from(pm, i, nw = n)
            #PM.constraint_thermal_limit_to(pm, i, nw = n)
        end
    end

    return
end

function instantiate_bfp_expr_model(data::Dict{String, Any}, model_constructor; kwargs...)
    return PM.instantiate_model(data, model_constructor, instantiate_bfp_expr; kwargs...)
end

""
# replicates PM.build_mn_opf_bf_strg
function instantiate_bfp_expr(pm::PM.AbstractPowerModel)
    for n in eachindex(PM.nws(pm))
        @assert !PM.ismulticonductor(pm, nw = n)
        PM.variable_bus_voltage(pm, nw = n)
        PM.variable_branch_power(pm, nw = n; bounded = false)
        PM.variable_dcline_power(pm, nw = n)

        PM.constraint_model_current(pm, nw = n)

        for i in PM.ids(pm, :ref_buses, nw = n)
            PM.constraint_theta_ref(pm, i, nw = n)
        end

        for i in PM.ids(pm, :bus, nw = n)
            constraint_power_balance_ni_expr(pm, i, nw = n)
        end

        for i in PM.ids(pm, :branch, nw = n)
            PM.constraint_power_losses(pm, i, nw = n)
            PM.constraint_voltage_magnitude_difference(pm, i, nw = n)

            PM.constraint_voltage_angle_difference(pm, i, nw = n)
        end

        for i in PM.ids(pm, :dcline, nw = n)
            PM.constraint_dcline_power_losses(pm, i, nw = n)
        end
    end

    return
end

function instantiate_vip_expr_model(data::Dict{String, Any}, model_constructor; kwargs...)
    throw(error("VI Models not currently supported"))
end

#################################################################################
# Model Extention Functions

""
function constraint_power_balance_ni_expr(
    pm::PM.AbstractPowerModel,
    i::Int;
    nw::Int = pm.cnw,
)
    if !haskey(PM.con(pm, nw), :power_balance_p)
        PM.con(pm, nw)[:power_balance_p] = Dict{Int, JuMP.ConstraintRef}()
    end
    if !haskey(PM.con(pm, nw), :power_balance_q)
        PM.con(pm, nw)[:power_balance_q] = Dict{Int, JuMP.ConstraintRef}()
    end

    bus_arcs = PM.ref(pm, nw, :bus_arcs, i)
    bus_arcs_dc = PM.ref(pm, nw, :bus_arcs_dc, i)

    inj_p_expr = PM.ref(pm, nw, :bus, i, "inj_p")
    inj_q_expr = PM.ref(pm, nw, :bus, i, "inj_q")

    constraint_power_balance_ni_expr(
        pm,
        nw,
        i,
        bus_arcs,
        bus_arcs_dc,
        inj_p_expr,
        inj_q_expr,
    )

    return
end

""
function constraint_power_balance_ni_expr(
    pm::PM.AbstractPowerModel,
    n::Int,
    i::Int,
    bus_arcs,
    bus_arcs_dc,
    inj_p_expr,
    inj_q_expr,
)
    p = PM.var(pm, n, :p)
    q = PM.var(pm, n, :q)
    p_dc = PM.var(pm, n, :p_dc)
    q_dc = PM.var(pm, n, :q_dc)

    PM.con(pm, n, :power_balance_p)[i] = JuMP.@constraint(
        pm.model,
        sum(p[a] for a in bus_arcs) + sum(p_dc[a_dc] for a_dc in bus_arcs_dc) == inj_p_expr
    )
    PM.con(pm, n, :power_balance_q)[i] = JuMP.@constraint(
        pm.model,
        sum(q[a] for a in bus_arcs) + sum(q_dc[a_dc] for a_dc in bus_arcs_dc) == inj_q_expr
    )

    return
end

""
function constraint_current_balance_ni_expr(
    pm::PM.AbstractPowerModel,
    i::Int;
    nw::Int = pm.cnw,
)
    if !haskey(PM.con(pm, nw), :kcl_cr)
        PM.con(pm, nw)[:kcl_cr] = Dict{Int, JuMP.ConstraintRef}()
    end
    if !haskey(PM.con(pm, nw), :kcl_ci)
        PM.con(pm, nw)[:kcl_ci] = Dict{Int, JuMP.ConstraintRef}()
    end

    bus_arcs = PM.ref(pm, nw, :bus_arcs, i)
    bus_arcs_dc = PM.ref(pm, nw, :bus_arcs_dc, i)

    inj_p_expr = PM.ref(pm, nw, :bus, i, "inj_p")
    inj_q_expr = PM.ref(pm, nw, :bus, i, "inj_q")

    constraint_current_balance_ni_expr(
        pm,
        nw,
        i,
        bus_arcs,
        bus_arcs_dc,
        inj_p_expr,
        inj_q_expr,
    )

    return
end

""
function constraint_current_balance_ni_expr(
    pm::PM.AbstractPowerModel,
    n::Int,
    i::Int,
    bus_arcs,
    bus_arcs_dc,
    inj_p_expr,
    inj_q_expr,
)
    p = PM.var(pm, n, :p)
    q = PM.var(pm, n, :q)
    p_dc = PM.var(pm, n, :p_dc)
    q_dc = PM.var(pm, n, :q_dc)

    PM.con(pm, n, :power_balance_p)[i] = JuMP.@constraint(
        pm.model,
        sum(p[a] for a in bus_arcs) + sum(p_dc[a_dc] for a_dc in bus_arcs_dc) == inj_p_expr
    )
    PM.con(pm, n, :power_balance_q)[i] = JuMP.@constraint(
        pm.model,
        sum(q[a] for a in bus_arcs) + sum(q_dc[a_dc] for a_dc in bus_arcs_dc) == inj_q_expr
    )

    return
end

"active power only models ignore reactive power variables"
function variable_reactive_net_injection(pm::PM.AbstractActivePowerModel; kwargs...)
    return
end

""
function constraint_power_balance_ni_expr(
    pm::PM.AbstractActivePowerModel,
    n::Int,
    i::Int,
    bus_arcs,
    bus_arcs_dc,
    inj_p_expr,
    _,
)
    p = PM.var(pm, n, :p)
    p_dc = PM.var(pm, n, :p_dc)

    PM.con(pm, n, :power_balance_p)[i] = JuMP.@constraint(
        pm.model,
        sum(p[a] for a in bus_arcs) + sum(p_dc[a_dc] for a_dc in bus_arcs_dc) == inj_p_expr
    )

    return
end

""
function powermodels_network!(
    optimization_container::OptimizationContainer,
    system_formulation::Type{S},
    sys::PSY.System,
    template::OperationsProblemTemplate,
    instantiate_model = instantiate_nip_expr_model,
) where {S <: PM.AbstractPowerModel}
    time_steps = model_time_steps(optimization_container)
    pm_data, PM_map = pass_to_pm(sys, template, time_steps[end])
    buses = PSY.get_components(PSY.Bus, sys)

    remove_undef!(optimization_container.expressions[:nodal_balance_active])
    remove_undef!(optimization_container.expressions[:nodal_balance_reactive])

    for t in time_steps, bus in buses
        pm_data["nw"]["$(t)"]["bus"]["$(bus.number)"]["inj_p"] =
            optimization_container.expressions[:nodal_balance_active][bus.number, t]
        pm_data["nw"]["$(t)"]["bus"]["$(bus.number)"]["inj_q"] =
            optimization_container.expressions[:nodal_balance_reactive][bus.number, t]
    end

    optimization_container.pm = instantiate_model(
        pm_data,
        system_formulation,
        jump_model = optimization_container.JuMPmodel,
    )
    optimization_container.pm.ext[:PMmap] = PM_map

    return
end

""
function powermodels_network!(
    optimization_container::OptimizationContainer,
    system_formulation::Type{S},
    sys::PSY.System,
    template::OperationsProblemTemplate,
    instantiate_model = instantiate_nip_expr_model,
) where {S <: PM.AbstractActivePowerModel}
    time_steps = model_time_steps(optimization_container)
    pm_data, PM_map = pass_to_pm(sys, template, time_steps[end])
    buses = PSY.get_components(PSY.Bus, sys)

    remove_undef!(optimization_container.expressions[:nodal_balance_active])

    for t in time_steps, bus in buses
        pm_data["nw"]["$(t)"]["bus"]["$(PSY.get_number(bus))"]["inj_p"] =
            optimization_container.expressions[:nodal_balance_active][
                PSY.get_number(bus),
                t,
            ]
        # pm_data["nw"]["$(t)"]["bus"]["$(bus.number)"]["inj_q"] = 0.0
    end

    optimization_container.pm = instantiate_model(
        pm_data,
        system_formulation,
        jump_model = optimization_container.JuMPmodel,
    )
    optimization_container.pm.ext[:PMmap] = PM_map

    return
end

#### PM accessor functions ########

function PMvarmap(::Type{S}) where {S <: PM.AbstractDCPModel}
    pm_var_map = Dict{Type, Dict{Symbol, Union{String, NamedTuple}}}()

    pm_var_map[PSY.Bus] = Dict(:va => THETA)
    pm_var_map[PSY.ACBranch] =
        Dict(:p => (from_to = FlowActivePowerVariable, to_from = nothing))
    pm_var_map[PSY.DCBranch] =
        Dict(:p_dc => (from_to = FlowActivePowerVariable, to_from = nothing))

    return pm_var_map
end

function PMvarmap(::Type{S}) where {S <: PM.AbstractActivePowerModel}
    pm_var_map = Dict{Type, Dict{Symbol, Union{String, NamedTuple}}}()

    pm_var_map[PSY.Bus] = Dict(:va => THETA)
    pm_var_map[PSY.ACBranch] = Dict(:p => FlowActivePowerFromToVariable)
    pm_var_map[PSY.DCBranch] = Dict(
        :p_dc => (
            from_to = FlowActivePowerFromToVariable,
            to_from = FlowActivePowerToFromVariable,
        ),
    )

    return pm_var_map
end

function PMvarmap(::Type{PTDFPowerModel})
    pm_var_map = Dict{Type, Dict{Symbol, Union{String, NamedTuple}}}()

    pm_var_map[PSY.Bus] = Dict()
    pm_var_map[PSY.ACBranch] = Dict()
    pm_var_map[PSY.DCBranch] = Dict()

    return pm_var_map
end

function PMvarmap(::Type{S}) where {S <: PM.AbstractPowerModel}
    pm_var_map = Dict{Type, Dict{Symbol, Union{String, NamedTuple}}}()

    pm_var_map[PSY.Bus] = Dict(:va => THETA, :vm => VM)
    pm_var_map[PSY.ACBranch] = Dict(
        :p => (
            from_to = FlowActivePowerFromToVariable,
            to_from = FlowActivePowerToFromVariable,
        ),
        :q => (
            from_to = FlowReactivePowerFromToVariable,
            to_from = FlowReactivePowerToFromVariable,
        ),
    )
    pm_var_map[PSY.DCBranch] = Dict(
        :p_dc => (from_to = FlowActivePowerVariable, to_from = nothing),
        :q_dc => (
            from_to = FlowReactivePowerFromToVariable,
            to_from = FlowReactivePowerToFromVariable,
        ),
    )

    return pm_var_map
end

function PMconmap(::Type{S}) where {S <: PM.AbstractActivePowerModel}
    pm_con_map = Dict{Type, Dict{Symbol, String}}()

    pm_con_map[PSY.Bus] = Dict(:power_balance_p => NODAL_BALANCE_ACTIVE)
    return pm_con_map
end

function PMconmap(::Type{S}) where {S <: PM.AbstractPowerModel}
    pm_con_map = Dict{Type, Dict{Symbol, String}}()

    pm_con_map[PSY.Bus] = Dict(
        :power_balance_p => NODAL_BALANCE_ACTIVE,
        :power_balance_q => NODAL_BALANCE_REACTIVE,
    )
    return pm_con_map
end

function PMexprmap(::Type{S}) where {S <: PM.AbstractPowerModel}
    pm_expr_map = Dict{
        Type,
        NamedTuple{
            (:pm_expr, :psi_con),
            Tuple{Dict{Symbol, Union{String, NamedTuple}}, Symbol},
        },
    }()

    return pm_expr_map
end

function PMexprmap(::Type{PTDFPowerModel})
    pm_expr_map = Dict{
        Type,
        NamedTuple{
            (:pm_expr, :psi_con),
            Tuple{Dict{Symbol, Union{String, NamedTuple}}, Symbol},
        },
    }()

    pm_expr_map[PSY.ACBranch] = (
        pm_expr = Dict(:p => (from_to = FlowActivePowerVariable, to_from = nothing)),
        psi_con = Symbol(NETWORK_FLOW),
    )

    return pm_expr_map
end

function add_pm_var_refs!(
    optimization_container::OptimizationContainer,
    system_formulation::Type{S},
    ::PSY.System,
) where {S <: PM.AbstractPowerModel}
    time_steps = model_time_steps(optimization_container)
    bus_dict = optimization_container.pm.ext[:PMmap].bus
    ACbranch_dict = optimization_container.pm.ext[:PMmap].arcs
    ACbranch_types = typeof.(values(ACbranch_dict))
    DCbranch_dict = optimization_container.pm.ext[:PMmap].arcs_dc
    DCbranch_types = typeof.(values(DCbranch_dict))

    pm_var_types = keys(PM.var(optimization_container.pm, 1))

    pm_var_map = PMvarmap(system_formulation)

    for (pm_v, ps_v) in pm_var_map[PSY.Bus]
        if pm_v in pm_var_types
            container = PSI.container_spec(
                JuMP.VariableRef,
                [PSY.get_name(b) for b in values(bus_dict)],
                time_steps,
            )
            assign_variable!(optimization_container, ps_v, PSY.Bus, container)
            for t in time_steps, (pm_bus, bus) in bus_dict
                name = PSY.get_name(bus)
                container[name, t] = PM.var(optimization_container.pm, t, pm_v)[pm_bus] # pm_vars[pm_v][pm_bus]
            end
        end
    end

    add_pm_var_refs!(
        optimization_container,
        PSY.ACBranch,
        ACbranch_types,
        ACbranch_dict,
        pm_var_map,
        pm_var_types,
        time_steps,
    )
    add_pm_var_refs!(
        optimization_container,
        PSY.DCBranch,
        DCbranch_types,
        DCbranch_dict,
        pm_var_map,
        pm_var_types,
        time_steps,
    )
end

function add_pm_var_refs!(
    optimization_container::OptimizationContainer,
    d_class::Type,
    device_types::Vector,
    pm_map::Dict,
    pm_var_map::Dict,
    pm_var_types::Base.KeySet,
    time_steps::UnitRange{Int},
)
    for d_type in Set(device_types)
        devices = [d for d in pm_map if typeof(d[2]) == d_type]
        for (pm_v, ps_v) in pm_var_map[d_class]
            if pm_v in pm_var_types
                for dir in fieldnames(typeof(ps_v))
                    var_type = getfield(ps_v, dir)
                    var_type === nothing && continue
                    container = PSI.container_spec(
                        JuMP.VariableRef,
                        [PSY.get_name(d[2]) for d in devices],
                        time_steps,
                    )
                    assign_variable!(
                        optimization_container,
                        make_variable_name(var_type, d_type),
                        container,
                    )
                    for t in time_steps, (pm_d, d) in devices
                        var =
                            PM.var(optimization_container.pm, t, pm_v, getfield(pm_d, dir))
                        container[PSY.get_name(d), t] = var
                    end
                end
            end
        end
    end
end

function add_pm_con_refs!(
    optimization_container::OptimizationContainer,
    system_formulation::Type{S},
    ::PSY.System,
) where {S <: PM.AbstractPowerModel}
    time_steps = model_time_steps(optimization_container)
    bus_dict = optimization_container.pm.ext[:PMmap].bus

    pm_con_names = [
        k for k in keys(PM.con(optimization_container.pm, 1)) if
        !isempty(PM.con(optimization_container.pm, 1, k))
    ]

    pm_con_map = PMconmap(system_formulation)
    for (pm_v, ps_v) in pm_con_map[PSY.Bus]
        if pm_v in pm_con_names
            container = PSI.add_cons_container!(
                optimization_container,
                make_constraint_name(ps_v, PSY.Bus),
                [PSY.get_name(b) for b in values(bus_dict)],
                time_steps,
            )
            for t in time_steps, (pm_bus, bus) in bus_dict
                name = PSY.get_name(bus)
                container[name, t] = PM.con(optimization_container.pm, t, pm_v)[pm_bus]
            end
        end
    end
end

function add_pm_expr_refs!(
    optimization_container::OptimizationContainer,
    system_formulation::Type{S},
    ::PSY.System,
) where {S <: PM.AbstractPowerModel}
    time_steps = model_time_steps(optimization_container)

    ACbranch_dict = optimization_container.pm.ext[:PMmap].arcs
    ACbranch_types = typeof.(values(ACbranch_dict))

    pm_var_types = keys(PM.var(optimization_container.pm, 1))
    pm_expr_map = PMexprmap(system_formulation)

    add_pm_expr_refs!(
        optimization_container,
        PSY.ACBranch,
        ACbranch_types,
        ACbranch_dict,
        pm_expr_map,
        pm_var_types,
        time_steps,
    )
end

function add_pm_expr_refs!(
    optimization_container::OptimizationContainer,
    d_class::Type,
    device_types::Vector,
    pm_map::Dict,
    pm_expr_map::Dict,
    pm_var_types::Base.KeySet,
    time_steps::UnitRange{Int},
)
    for d_type in Set(device_types)
        for (pm_expr_var, ps_v) in pm_expr_map[d_class].pm_expr
            if pm_expr_var in pm_var_types
                pm_devices = keys(PM.var(optimization_container.pm, pm_expr_var, nw = 1))
                mapped_pm_devices = Vector()
                mapped_ps_devices = Vector{d_type}()
                for d in pm_map
                    if typeof(d[2]) == d_type &&
                       d[1].from_to ∈ pm_devices &&
                       d[1].to_from ∈ pm_devices
                        push!(mapped_pm_devices, d[1])
                        push!(mapped_ps_devices, d[2])
                    end
                end
                isempty(mapped_ps_devices) && continue
                mapped_ps_device_names = PSY.get_name.(mapped_ps_devices)

                # add variable in psi
                # add psi_var = pm_expr_var as constraint
                for dir in fieldnames(typeof(ps_v))
                    var_type = getfield(ps_v, dir)
                    var_type === nothing && continue

                    add_variable!(
                        optimization_container,
                        var_type(),
                        mapped_ps_devices,
                        StaticBranchUnbounded(),
                    )
                    psi_var_container = get_variable(
                        optimization_container,
                        make_variable_name(var_type, d_type),
                    )

                    con_name = make_constraint_name(pm_expr_map[d_class].psi_con, d_type)
                    psi_con_container = add_cons_container!(
                        optimization_container,
                        con_name,
                        mapped_ps_device_names,
                        time_steps,
                    )
                    for t in time_steps,
                        (pm_d, name) in zip(mapped_pm_devices, mapped_ps_device_names)

                        pm_expr = PM.var(
                            optimization_container.pm,
                            t,
                            pm_expr_var,
                            getfield(pm_d, dir),
                        )
                        psi_con_container[name, t] = JuMP.@constraint(
                            optimization_container.JuMPmodel,
                            psi_var_container[name, t] == pm_expr
                        )
                    end
                end
            end
        end
    end
end


#
println("Af) Test of the frequency-independent Breit interaction for an internally generated chlorine-like multiplet.")
#
wa = Atomic.Computation(Atomic.Computation(), name="xx", grid=JAC.Radial.Grid(true), nuclearModel=Nuclear.Model(54.),
                        configs=[Configuration("[Ne] 3s^2 3p^5"), Configuration("[Ne] 3s 3p^6"), Configuration("[Ne] 3s^2 3p^4 3d")],
                        asfSettings=AsfSettings(true, false, Basics.DFSField(), "hydrogenic", Dict{Subshell, Orbital}(), [1],    40, 1.0e-6, JAC.Subshell[], JAC.Subshell[],
                                                true, false, NoneQed(), LSjjSettings(true),
                                                false, [1,2,3,4], false, JAC.LevelSymmetry[] )  )

wb = perform(wa)


# -----------------------------------------------------------------------------
# Integration with AtomsBase
# -----------------------------------------------------------------------------

# Internal type used to represent a MassBody and retain the element metadata
struct ElementMassBody{cType<:Real,mType<:Real} <: Body
    r::SVector{3,cType} # in LENGTH_UNIT
    v::SVector{3,cType} # in VELOCITY_UNIT
    m::mType            # in MASS_UNIT
    symbol::Symbol
    number::Int
    data::Dict{Symbol,Any}
end
function ElementMassBody(r::SVector{3,<:Unitful.Length}, v::SVector{3,<:Unitful.Velocity}, e::Element; data...)
    ElementMassBody{Float64,Float64}(austrip.(r), austrip.(v), austrip(e.atomic_mass), Symbol(e.symbol), e.number, Dict(data...))
end
function ElementMassBody(body::ElementMassBody{cType,mType}, r::SVector{3,cType}, v::SVector{3,cType}) where {cType<:Real,mType<:Real}
    ElementMassBody{cType,mType}(r, v, body.m, body.symbol, body.number, body.data)
end

# Convert AtomsBase Atom to NBodySimulator body
function ElementMassBody(atom::Atom)
    ElementMassBody{Float64,Float64}(
        austrip.(position(atom)),
        austrip.(velocity(atom)),
        austrip(atomic_mass(atom)),
        AtomsBase.atomic_symbol(atom),
        atomic_number(atom),
        atom.data
    )
end
# Convert NBodySimulator body to AtomsBase Atom
# TODO: support more boundary conditions
function AtomsBase.Atom(b::ElementMassBody, boundary_conditions::CubicPeriodicBoundaryConditions)
    Atom(b.symbol, mod.(b.r, boundary_conditions.L) .* LENGTH_UNIT, b.v .* VELOCITY_UNIT; b.data...)
end

# Convert AtomsBase boundary conditions to NBodySimulator boundary conditions
function nbs_boundary_conditions(system::AbstractSystem{3})
    # TODO: support more boundary conditions
    box = bounding_box(system)
    @assert all(ustrip(box[i][j]) == 0 for i ∈ 1:3 for j ∈ 1:3 if i != j)
    @assert box[1][1] == box[2][2] == box[3][3]
    @assert boundary_conditions(system) == [Periodic(), Periodic(), Periodic()]
    CubicPeriodicBoundaryConditions(austrip(box[1][1]))
end
# Convert NBodySimulator boundary conditions to AtomsBase boundary conditions
# TODO: support more boundary conditions
ab_boundary_conditions(::CubicPeriodicBoundaryConditions) = [Periodic(), Periodic(), Periodic()]

# Convert NBodySimulator boundary conditions to AtomsBase bounding box
# TODO: support more boundary conditions
function ab_bounding_box(boundary_conditions::CubicPeriodicBoundaryConditions)
    box_size = boundary_conditions.L * LENGTH_UNIT
    z = zero(typeof(box_size))
    [[box_size, z, z], [z, box_size, z], [z, z, box_size]]
end

# Convert AtomsBase AbstractSystem to Vector of NBodySimulator bodies
bodies(system::AbstractSystem{3}) = ElementMassBody.(system)
# Convert Vector of NBodySimulator bodies to AtomsBase FlexibleSystem
function AtomsBase.FlexibleSystem(bodies::AbstractVector{<:ElementMassBody}, boundary_conditions::BoundaryConditions)
    particles = Fix2(Atom, boundary_conditions).(bodies)
    FlexibleSystem(particles, ab_bounding_box(boundary_conditions), ab_boundary_conditions(boundary_conditions))
end
# Convert Vector of NBodySimulator bodies to AtomsBase FastSystem
function AtomsBase.FastSystem(bodies::AbstractVector{<:ElementMassBody}, boundary_conditions::BoundaryConditions)
    particles = Fix2(Atom, boundary_conditions).(bodies)
    FastSystem(particles, ab_bounding_box(boundary_conditions), ab_boundary_conditions(boundary_conditions))
end

# -----------------------------------------------------------------------------
# Convenience methods for generating starting configurations with element data
# -----------------------------------------------------------------------------

function NBodySimulator.generate_bodies_in_cell_nodes(n::Integer, symbol::Union{Integer,AbstractString,Symbol}, L::Unitful.Length, reference_temp::Unitful.Temperature; rng = MersenneTwister(n))
    e = elements[symbol]
    average_velocity = √(u"k" * reference_temp / e.atomic_mass)
    generate_bodies_in_cell_nodes(n, symbol, average_velocity, L, rng = rng)
end
function NBodySimulator.generate_bodies_in_cell_nodes(n::Integer, symbol::Union{Integer,AbstractString,Symbol}, average_velocity::Unitful.Velocity, L::Unitful.Length; rng = MersenneTwister(n))
    velocities = average_velocity * randn(rng, Float64, (3, n))
    e = elements[symbol]
    bodies = ElementMassBody[]

    count = 1
    dL = L / (ceil(n^(1 / 3)))
    for x ∈ dL/2:dL:L, y ∈ dL/2:dL:L, z ∈ dL/2:dL:L
        if count > n
            break
        end
        push!(bodies, ElementMassBody(SVector(x, y, z), SVector{3}(velocities[:, count]), e))
        count += 1
    end
    return bodies
end


module BinaryLambdaCalculus

include("coding.jl")
include("grygiel_lescanne.jl")

end


module Vlasiator

using Requires
using StaticArrays: SVector, @SVector, SMatrix, @SMatrix
using Printf: @sprintf
using LinearAlgebra: ×, dot, ⋅, norm, normalize, normalize!
using Statistics: mean
using EzXML
using Mmap: mmap
using WriteVTK
using LazyGrids: ndgrid
using LaTeXStrings
using Dates

include("utility/rotation.jl")
include("utility/log.jl")
include("utility/monitor.jl")
include("utility/array.jl")
include("utility/fluxfunction.jl")
include("vlsv/vlsvreader.jl")
include("vlsv/vlsvutility.jl")
include("utility/plot.jl")

export
   # vlsvreader
   MetaVLSV, VarInfo,
   load, readvariable, readparameter, readvariablemeta, readvcells,
   hasvariable, hasparameter, hasname,
   # vlsvutility
   getcell, getslicecell, getlevel, refineslice, getcellcoordinates, getvcellcoordinates,
   getchildren, getparent, isparent, getsiblings,
   getcellinline, getnearestcellwithvdf, getcellwithvdf,
   getdensity, getvelocity, getpressure, getmaxwellianity, write_vtk, write_vlsv, issame,
   # plot helper
   SI, EARTH, Log, Linear, SymLog,
   # log
   readlog,
   # physical parameter monitor
   check_plasma_characteristics,
   # fluxfunction
   compute_flux_function, find_reconnection_points

precompile(load, (String,))
precompile(readvariable, (MetaVLSV, String, Bool))

function __init__()
   @require PyPlot="d330b81b-6aea-500a-939a-2ce795aea3ee" begin
      include("plot/pyplot.jl")
   end
   @require Plots="91a5bcdd-55d7-5caf-9e0b-520d859cae80" begin
      include("plot/plots.jl")
   end
end

end


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

using FEMBase, MortarContact2D, Test, SparseArrays, LinearAlgebra, Statistics

@testset "MortarContact2D.jl" begin

    @testset "Projecting vertices between surfaces" begin
        include("test_mortar2d_calculate_projection.jl")
    end

    @testset "Mortar coupling, example 1" begin
        include("test_mortar2d_ex1.jl")
    end

    @testset "Mortar coupling, example 2" begin
        include("test_mortar2d_ex2.jl")
    end

    @testset "Contact basic test" begin
        include("test_contact2d.jl")
    end

    @testset "Testing contact segmentation" begin
        include("test_contact_segmentation.jl")
    end

end


ff_promote_rule(::Type{T1}, ::Type{T2}, ::Val{W}) where {T1,T2,W} = promote_type(T1,T2)

function _ff_promote_rule(::Type{T1}, ::Type{T2}, ::Val{W}) where {T1, T2, W}
    T_canon = promote_type(T1,T2)
    ifelse(lt(pick_vector_width(T_canon), StaticInt{W}()), T1, T_canon)
end
function __ff_maybe_promote_int(::True, ::Type{T}, ::Type{T_canon}, ::Val{W}) where {T,T_canon,W}
    ifelse(eq(static_sizeof(T_canon), static_sizeof(T)), T, pick_integer(Val{W}(), T_canon))
end
__ff_maybe_promote_int(::False, ::Type{T1}, ::Type{T_canon}, ::Val{W}) where {T1,T_canon,W} = T_canon
function _ff_promote_rule(::Type{T1}, ::Type{T2}, ::Val{W}) where {T1<:Integer, T2<:Integer, W}
    T_canon = promote_type(T1,T2)
    __ff_maybe_promote_int(lt(pick_vector_width(T_canon), StaticInt{W}()), T1, T_canon, Val{W}())
end
ff_promote_rule(::Type{T1}, ::Type{T2}, ::Val{W}) where {T1 <: Integer, T2 <: Integer,W} = _ff_promote_rule(T1,T2,Val{W}())
ff_promote_rule(::Type{T1}, ::Type{T2}, ::Val{W}) where {T1 <: FloatingTypes, T2<:FloatingTypes,W} = _ff_promote_rule(T1,T2,Val{W}())

Base.promote_rule(::Type{V}, ::Type{T2}) where {W,T1,T2<:NativeTypes,V<:AbstractSIMDVector{W,T1}} = Vec{W,ff_promote_rule(T1,T2,Val{W}())}
Base.promote_rule(::Type{V}, ::Type{Bool}) where {V<:AbstractMask} = V

_assemble_vec_unroll(::Val{N}, ::Type{V}) where {N,W,T,V<:AbstractSIMDVector{W,T}} = VecUnroll{N,W,T,V}
_assemble_vec_unroll(::Val{N}, ::Type{T}) where {N,T<:NativeTypes} = VecUnroll{N,1,T,T}
Base.promote_rule(::Type{VecUnroll{N,W,T1,V}}, ::Type{T2}) where {N,W,T1,V,T2<:NativeTypes} = _assemble_vec_unroll(Val{N}(), promote_type(V,T2))
Base.promote_rule(::Type{VecUnroll{N,W,T,V1}}, ::Type{V2}) where {N,W,T,V1,T2,V2<:AbstractSIMDVector{W,T2}} = _assemble_vec_unroll(Val{N}(), promote_type(V1,V2))
# Base.promote_rule(::Type{VecUnroll{N,W,T,V1}}, ::Type{V2}) where {N,W,T,V1,V2<:AbstractSIMDVector{W}} = _assemble_vec_unroll(Val{N}(), promote_type(V1,V2))
Base.promote_rule(::Type{VecUnroll{N,W,T,V1}}, ::Type{V2}) where {N,W,T,V1,V2<:AbstractMask{W}} = _assemble_vec_unroll(Val{N}(), promote_type(V1,V2))
Base.promote_rule(::Type{VecUnroll{N,W,T1,V1}}, ::Type{VecUnroll{N,W,T2,V2}}) where {N,W,T1,T2,V1,V2} = _assemble_vec_unroll(Val{N}(), promote_type(V1,V2))

Base.promote_rule(::Type{VecUnroll{N,W,T1,V1}}, ::Type{VecUnroll{N,1,T2,T2}}) where {N,W,T1,T2,V1} = _assemble_vec_unroll(Val{N}(), promote_type(V1,T2))
Base.promote_rule(::Type{VecUnroll{N,1,T1,T1}}, ::Type{VecUnroll{N,W,T2,V2}}) where {N,W,T1,T2,V2} = _assemble_vec_unroll(Val{N}(), promote_type(T1,V2))
Base.promote_rule(::Type{VecUnroll{N,1,T1,T1}}, ::Type{VecUnroll{N,1,T2,T2}}) where {N,T1,T2} = _assemble_vec_unroll(Val{N}(), promote_type(T1,T2))

Base.promote_rule(::Type{Mask{W,U}}, ::Type{EVLMask{W,U}}) where {W,U} = Mask{W,U}
Base.promote_rule(::Type{EVLMask{W,U}}, ::Type{Mask{W,U}}) where {W,U} = Mask{W,U}
Base.promote_rule(::Type{Bit}, ::Type{T}) where {T <: Number} = T

issigned(x) = issigned(typeof(x))
issigned(::Type{<:Signed}) = True()
issigned(::Type{<:Unsigned}) = False()
issigned(::Type{<:AbstractSIMD{<:Any,T}}) where {T} = issigned(T)
issigned(::Type{T}) where {T} = nothing
"""
 Promote, favoring <:Signed or <:Unsigned of first arg.
"""
@inline promote_div(x::Union{Integer,AbstractSIMD{<:Any,<:Integer}}, y::Union{Integer,AbstractSIMD{<:Any,<:Integer}}) = promote_div(x, y, issigned(x))
@inline promote_div(x, y) = promote(x, y)
@inline promote_div(x, y, ::Nothing) = promote(x, y) # for Integers that are neither Signed or Unsigned, e.g. Bool
@inline function promote_div(x::T1, y::T2, ::True) where {T1,T2}
    T = promote_type(T1, T2)
    signed(x % T), signed(y % T)
end
@inline function promote_div(x::T1, y::T2, ::False) where {T1,T2}
    T = promote_type(T1, T2)
    unsigned(x % T), unsigned(y % T)
end
itosize(i::Union{I,AbstractSIMD{<:Any,I}}, ::Type{J}) where {I,J} = signorunsign(i % J, issigned(I))
signorunsign(i, ::True) = signed(i)
signorunsign(i, ::False) = unsigned(i)

# Base.promote_rule(::Type{VecTile{M,N,W,T1}}, ::Type{T2}) where {M,N,W,T1,T2<:NativeTypes} = VecTile{M,N,W,promote_rule(T1,T2)}
# Base.promote_rule(::Type{VecTile{M,N,W,T1}}, ::Type{Vec{W,T2}}) where {M,N,W,T1,T2} = VecTile{M,N,W,promote_rule(T1,T2)}
# Base.promote_rule(::Type{VecTile{M,N,W,T1}}, ::Type{VecUnroll{M,W,T2}}) where {M,N,W,T1,T2} = VecTile{M,N,W,promote_rule(T1,T2)}
# Base.promote_rule(::Type{VecTile{M,N,W,T1}}, ::Type{VecTile{M,N,W,T2}}) where {M,N,W,T1,T2} = VecTile{M,N,W,promote_rule(T1,T2)}

@generated function _ff_promote_rule(::Type{T1}, ::Type{T2}, ::Val{W}, ::StaticInt{RS}) where {T1 <: IntegerTypes, T2 <: FloatingTypes,W, RS}
    T_canon = promote_type(T1,T2)
    (sizeof(T_canon) * W ≤ RS) && return T_canon
    @assert sizeof(T1) * W ≤ RS
    @assert sizeof(T1) == 4
    Float32
end
@inline function ff_promote_rule(::Type{T1}, ::Type{T2}, ::Val{W}) where {T1 <: IntegerTypes, T2 <: FloatingTypes,W}
    _ff_promote_rule(T1, T2, Val{W}(), register_size())
end
@generated function _promote_rule(
    ::Type{V1}, ::Type{V2}, ::StaticInt{RS}
) where {W,T1,T2,V1<:AbstractSIMDVector{W,T1},V2<:AbstractSIMDVector{W,T2},RS}
    T = promote_type(T1,T2) # `T1` and `T2` should be defined in `Base`
    if RS ≥ W * sizeof(T)
        return :(Vec{$W,$T})
    end
    if T === Float64 || T === Float32
        N, r1 = (sizeof(T) * W) ÷ RS
        Wnew, r2 = divrem(W, N)
        @assert iszero(r)

        return :(VecUnroll{$(N-1),$Wnew,$T,Vec{$Wnew,$T}})
        # Should we demote `Float64` -> `Float32`?
        # return :(Vec{$W,$T})
        # don't demote to smaller than `Float32`
        # return :(Vec{$W,Float32})
    end
    # They're both of integers
    V1MM = V1 <: MM
    V2MM = V2 <: MM
    if V1MM ⊻ V2MM
        V1MM ? :(Vec{$W,$T2}) : :(Vec{$W,$T1})
    else # either both are `MM` or neither are
        B = W ÷ sizeof(T)
        if !V1MM # if neither are
            B = max(4, B)
        end
        I = integer_of_bytes_symbol(B, T <: Unsigned)
        :(Vec{$W,$I})
    end
end
@inline function Base.promote_rule(::Type{V1}, ::Type{V2}) where {W,T1,T2,V1<:AbstractSIMDVector{W,T1},V2<:AbstractSIMDVector{W,T2}}
    _promote_rule(V1, V2, register_size(promote_type(T1,T2)))
end

maybethrow(::True) = throw(ArgumentError("The arguments were invalid."))
maybethrow(::False) = nothing

# not @generated, because calling `promote_type` on vector types
@inline function Base.promote_rule(
    ::Type{VecUnroll{Nm1,Wsplit,T,V1}}, ::Type{V2}
) where {Nm1,Wsplit,T,V1,T2,W,V2<:AbstractSIMDVector{W,T2}}
    maybethrow(ArrayInterface.ne(StaticInt{Nm1}() * StaticInt{Wsplit}() + StaticInt{Wsplit}(), StaticInt{W}()))
    V3 = Vec{Wsplit,T2}
    _assemble_vec_unroll(Val{Nm1}(), promote_type(V1,V3))
end
@inline function Base.promote_rule(
    ::Type{VecUnroll{Nm1,Wsplit,T,V1}}, ::Type{V2}
) where {Nm1,Wsplit,T,V1,W,V2<:AbstractMask{W}}
    maybethrow(ArrayInterface.ne(StaticInt{Nm1}() * StaticInt{Wsplit}() + StaticInt{Wsplit}(), StaticInt{W}()))
    V3 = Mask{Wsplit,mask_type(StaticInt{Wsplit}())}
    _assemble_vec_unroll(Val{Nm1}(), promote_type(V1,V3))
end

@inline function Base.promote_rule(
    ::Type{VecUnroll{Nm1,1,T,T}}, ::Type{V2}
) where {Nm1,T,T2,W,V2<:AbstractSIMDVector{W,T2}}
    _assemble_vec_unroll(Val{Nm1}(), promote_type(T,V2))
end
@inline function Base.promote_rule(
    ::Type{VecUnroll{Nm1,1,T,T}}, ::Type{V2}
) where {Nm1,T,W,V2<:AbstractMask{W}}
    _assemble_vec_unroll(Val{Nm1}(), promote_type(T,V2))
end


using TextAnalysis

function setup()
    #opening the txt file containing all the information, and automatically closes it when done
    open("amazonreviews/test.ft.txt") do file

        #gathering all the data from the txt file
        global mydata = readlines(file)
    end

    #defining the pre-trained model to be used
    global model = SentimentAnalyzer()
end

function analysis()
    #defining two counters which will count the number of reviews analysed and number of correct analysis respectively
    iterations, correct = 0, 0

    #for loop for each line in the txt file
    for num in 1:length(mydata)

        #try/catch loop to catch out errors, explained later on
        try

            #getting the sentiment value from the txt file, 2 = positive, 1 = negative
            sentiment = parse(Int64,string(mydata[num][10]))

            #getting the review as a string
            sd = StringDocument(mydata[num][12:end])

            #removing corrupted characters
            remove_corrupt_utf8!(sd)

            #plugging the review into the model to get a floating point number ranging from 0-1, 1 = positive, 0 = negative
            value = model(sd)

            #adding one to the number of reviews analysed, to be displayed at the end
            iterations = iterations + 1

            #when both are positive (ie. analysed correctly), add one to the number of correct interpretations
            if value > 0.5 && sentiment == 2
                correct = correct + 1

            #when both are negative (ie. analysed correctly), add one to the number of correct interpretations
            elseif value < 0.5 && sentiment == 1
                correct = correct + 1
            end

        #catches out the BoundsError problem
        catch

            #skips the rest of the iteration to ignore the review and move onto the next (by the for loop)
            continue
        end
    end

    #print out number of interpretations
    println("number of interpretations: ", iterations)

    #print out number of correct interpretations
    println("number of correct interpretations: ", correct)

    #print out accuracy as a percentage (ie. how many of those interpretations were correct)
    println("accuracy: ", (correct/iterations*100),"%")
end

setup()
analysis()

# Results:
# Number of interpretations: 399703
# Number of correct interpretations: 207103
# Accuracy: 51.814222059879455%


for i in ("1", "01", "10", "11", "12", "20", "21")
    f = "cuda$(i)test.jl"
    info(f)
    include("../src/$f")
end

# Sun Aug 28 18:50:55 EEST 2016
#
# julia> Pkg.test("Knet")
# INFO: Testing Knet
# sqrt
#   1.198008 seconds (306.34 k allocations: 6.399 MB)
#   1.488229 seconds (305.40 k allocations: 6.363 MB)
# exp
#   0.765560 seconds (298.98 k allocations: 6.088 MB, 0.36% gc time)
#   1.539859 seconds (298.98 k allocations: 6.088 MB)
# log
#   0.740429 seconds (298.98 k allocations: 6.088 MB, 0.32% gc time)
#   1.654432 seconds (298.98 k allocations: 6.088 MB)
# sin
#   0.782896 seconds (298.98 k allocations: 6.088 MB)
#   1.974086 seconds (298.98 k allocations: 6.088 MB, 0.10% gc time)
# tanh
#   0.949269 seconds (298.98 k allocations: 6.088 MB)
#   1.781415 seconds (298.98 k allocations: 6.088 MB)
# neg
#   0.783159 seconds (298.98 k allocations: 6.088 MB)
#   1.411126 seconds (298.98 k allocations: 6.088 MB)
# inv
#   0.733850 seconds (298.98 k allocations: 6.088 MB, 0.23% gc time)
#   1.469844 seconds (298.98 k allocations: 6.088 MB)
# relu
#   0.734503 seconds (298.98 k allocations: 6.088 MB)
#   1.428948 seconds (298.98 k allocations: 6.088 MB)
# sigm
#   0.782921 seconds (298.98 k allocations: 6.088 MB)
#   1.572042 seconds (298.98 k allocations: 6.088 MB, 0.11% gc time)
# abs
#   0.754620 seconds (298.98 k allocations: 6.088 MB)
#   1.433652 seconds (298.98 k allocations: 6.088 MB, 0.11% gc time)
# abs2
#   0.734661 seconds (298.98 k allocations: 6.088 MB)
#   1.411047 seconds (298.98 k allocations: 6.088 MB)
# add
#   0.742177 seconds (305.10 k allocations: 6.339 MB)
#   1.407227 seconds (305.10 k allocations: 6.339 MB)
# sub
#   0.735742 seconds (298.98 k allocations: 6.088 MB, 0.25% gc time)
#   1.399725 seconds (298.98 k allocations: 6.088 MB)
# mul
#   0.735076 seconds (298.98 k allocations: 6.088 MB)
#   1.399998 seconds (298.98 k allocations: 6.088 MB)
# div
#   0.740939 seconds (298.98 k allocations: 6.088 MB)
#   1.534365 seconds (298.98 k allocations: 6.088 MB, 0.11% gc time)
# pow
#   0.745232 seconds (298.98 k allocations: 6.088 MB)
#   2.975677 seconds (298.98 k allocations: 6.088 MB)
# max
#   0.744763 seconds (298.98 k allocations: 6.088 MB)
#   1.408032 seconds (298.98 k allocations: 6.088 MB)
# min
#   0.743997 seconds (298.98 k allocations: 6.088 MB, 0.22% gc time)
#   1.406233 seconds (298.98 k allocations: 6.088 MB)
# pow
#   0.772437 seconds (305.10 k allocations: 6.339 MB, 0.23% gc time)
#   2.863349 seconds (305.11 k allocations: 6.339 MB)
# pow
#   0.765591 seconds (298.98 k allocations: 6.088 MB)
#   2.856764 seconds (298.98 k allocations: 6.088 MB, 0.06% gc time)
# pow
#   0.766089 seconds (298.98 k allocations: 6.088 MB)
#   2.856661 seconds (298.98 k allocations: 6.088 MB)
# add
#   0.840574 seconds (305.31 k allocations: 6.350 MB)
#   2.068473 seconds (305.32 k allocations: 6.350 MB)
# sub
#   0.833583 seconds (298.98 k allocations: 6.088 MB, 0.30% gc time)
#   2.061315 seconds (298.98 k allocations: 6.088 MB)
# mul
#   0.833596 seconds (298.98 k allocations: 6.088 MB, 0.27% gc time)
#   2.060891 seconds (298.98 k allocations: 6.088 MB)
# div
#   0.837831 seconds (298.98 k allocations: 6.088 MB)
#   2.096731 seconds (298.98 k allocations: 6.088 MB, 0.09% gc time)
# pow
#   0.937980 seconds (298.98 k allocations: 6.088 MB)
#   3.113095 seconds (298.98 k allocations: 6.088 MB, 0.06% gc time)
# max
#   0.836112 seconds (298.98 k allocations: 6.088 MB)
#   2.060026 seconds (298.98 k allocations: 6.088 MB)
# min
#   0.836224 seconds (298.98 k allocations: 6.088 MB)
#   2.059889 seconds (298.98 k allocations: 6.088 MB)
# add
#   0.918067 seconds (310.71 k allocations: 6.592 MB)
#   1.105991 seconds (309.96 k allocations: 6.552 MB)
#   1.283519 seconds (298.98 k allocations: 6.088 MB)
#   1.520820 seconds (298.98 k allocations: 6.088 MB)
#   2.091047 seconds (310.71 k allocations: 6.590 MB, 0.12% gc time)
#   1.444449 seconds (309.95 k allocations: 6.552 MB)
#   1.479492 seconds (298.98 k allocations: 6.088 MB)
#   1.599586 seconds (298.98 k allocations: 6.088 MB)
# sub
#   0.897188 seconds (298.98 k allocations: 6.088 MB, 0.25% gc time)
#   1.091974 seconds (298.98 k allocations: 6.088 MB)
#   1.310290 seconds (298.98 k allocations: 6.088 MB, 0.16% gc time)
#   1.576266 seconds (298.98 k allocations: 6.088 MB)
#   2.078716 seconds (298.98 k allocations: 6.088 MB)
#   1.434182 seconds (298.98 k allocations: 6.088 MB, 0.14% gc time)
#   1.482647 seconds (298.98 k allocations: 6.088 MB)
#   1.600311 seconds (298.98 k allocations: 6.088 MB, 0.11% gc time)
# mul
#   0.899609 seconds (298.98 k allocations: 6.088 MB)
#   1.091918 seconds (298.98 k allocations: 6.088 MB)
#   1.310454 seconds (298.98 k allocations: 6.088 MB, 0.14% gc time)
#   1.573036 seconds (298.98 k allocations: 6.088 MB)
#   2.078040 seconds (298.98 k allocations: 6.088 MB)
#   1.433069 seconds (298.98 k allocations: 6.088 MB)
#   1.478995 seconds (298.98 k allocations: 6.088 MB)
#   1.599840 seconds (298.98 k allocations: 6.088 MB, 0.15% gc time)
# div
#   0.898756 seconds (298.98 k allocations: 6.088 MB)
#   1.076243 seconds (298.98 k allocations: 6.088 MB)
#   1.273588 seconds (298.98 k allocations: 6.088 MB)
#   1.535823 seconds (298.98 k allocations: 6.088 MB)
#   2.122483 seconds (298.98 k allocations: 6.088 MB, 0.09% gc time)
#   1.561032 seconds (298.98 k allocations: 6.088 MB)
#   1.649356 seconds (298.98 k allocations: 6.088 MB)
#   1.834667 seconds (298.98 k allocations: 6.088 MB)
# pow
#   0.977069 seconds (298.98 k allocations: 6.088 MB)
#   1.097480 seconds (298.98 k allocations: 6.088 MB, 0.15% gc time)
#   1.308324 seconds (298.98 k allocations: 6.088 MB)
#   1.570295 seconds (298.98 k allocations: 6.088 MB)
#   3.174292 seconds (298.98 k allocations: 6.088 MB, 0.06% gc time)
#   3.131706 seconds (298.98 k allocations: 6.088 MB)
#   3.351452 seconds (298.98 k allocations: 6.088 MB, 0.05% gc time)
#   3.524023 seconds (298.98 k allocations: 6.088 MB)
# max
#   0.904631 seconds (298.98 k allocations: 6.088 MB)
#   1.078452 seconds (298.98 k allocations: 6.088 MB)
#   1.278753 seconds (298.98 k allocations: 6.088 MB)
#   1.542877 seconds (298.98 k allocations: 6.088 MB, 0.11% gc time)
#   2.083428 seconds (298.98 k allocations: 6.088 MB)
#   1.434031 seconds (298.98 k allocations: 6.088 MB)
#   1.477127 seconds (298.98 k allocations: 6.088 MB)
#   1.599171 seconds (298.98 k allocations: 6.088 MB)
# min
#   0.904480 seconds (298.98 k allocations: 6.088 MB, 0.18% gc time)
#   1.078259 seconds (298.98 k allocations: 6.088 MB)
#   1.278503 seconds (298.98 k allocations: 6.088 MB)
#   1.542717 seconds (298.98 k allocations: 6.088 MB, 0.12% gc time)
#   2.083427 seconds (298.98 k allocations: 6.088 MB)
#   1.434334 seconds (298.98 k allocations: 6.088 MB, 0.12% gc time)
#   1.478011 seconds (298.98 k allocations: 6.088 MB)
#   1.599201 seconds (298.98 k allocations: 6.088 MB)
# add
#   2.964730 seconds (404.17 k allocations: 7.825 MB)
#   3.059696 seconds (404.17 k allocations: 7.825 MB)
# add
#   2.958643 seconds (398.98 k allocations: 7.614 MB, 0.07% gc time)
#   3.053202 seconds (398.98 k allocations: 7.614 MB)
# mul
#   2.956758 seconds (398.98 k allocations: 7.614 MB, 0.07% gc time)
#   3.041163 seconds (398.98 k allocations: 7.614 MB)
# mul
#   2.954647 seconds (398.98 k allocations: 7.614 MB)
#   3.045259 seconds (398.98 k allocations: 7.614 MB, 0.07% gc time)
# max
#   2.989635 seconds (398.98 k allocations: 7.614 MB)
#   3.085604 seconds (398.98 k allocations: 7.614 MB, 0.07% gc time)
# max
#   2.981864 seconds (398.98 k allocations: 7.614 MB)
#   3.084174 seconds (398.98 k allocations: 7.614 MB)
# min
#   2.985036 seconds (398.98 k allocations: 7.614 MB, 0.07% gc time)
#   3.081865 seconds (398.98 k allocations: 7.614 MB)
# min
#   2.989373 seconds (398.98 k allocations: 7.614 MB, 0.07% gc time)
#   3.082634 seconds (398.98 k allocations: 7.614 MB)
# sum1
#   2.953099 seconds (398.98 k allocations: 7.614 MB)
#   3.045103 seconds (398.98 k allocations: 7.614 MB, 0.06% gc time)
# sum1
#   2.950990 seconds (398.98 k allocations: 7.614 MB)
#   3.051839 seconds (398.98 k allocations: 7.614 MB)
# sum2
#   2.952941 seconds (398.98 k allocations: 7.614 MB, 0.07% gc time)
#   3.045060 seconds (398.98 k allocations: 7.614 MB)
# sum2
#   2.960478 seconds (398.98 k allocations: 7.614 MB, 0.06% gc time)
#   3.046865 seconds (398.98 k allocations: 7.614 MB)
# nnz
#   2.977525 seconds (398.98 k allocations: 7.614 MB)
#   3.085898 seconds (398.98 k allocations: 7.614 MB, 0.06% gc time)
# nnz
#   2.984819 seconds (398.98 k allocations: 7.614 MB)
#   3.083199 seconds (398.98 k allocations: 7.614 MB)
# INFO: Knet tests passed


function args_train(s)
  @add_arg_table s begin
    "--model-type", "-m"
      help = "model to be used - one of 'unetlike', 'multiscale3', 'fcrna'"
      arg_type = String
      default = "multiscale3"
    "--patch-number", "-n"
      help = "number of patches used for training"
      arg_type = Int
      default = 10
    "--patch-size", "-s"
      help = "number of patches used for training"
      arg_type = Int
      default = 10
    "--test-size", "--testset-size", "-t"
      help = "number of patches used for testing"
      arg_type = Int
      default = 5
    "--batch-size", "-b"
      help = "number of patches per mini-batch during training"
      arg_type = Int
      default = 1
    "--batch-normalization", "-B"
      help = "whether to activate batch normalization"
      action = :store_true
    "--no-gpu"
      help = "prevent usage of gpu acceleration even if gpu support is detected"
      action = :store_true
    "--epochs", "-e"
      help = "number of training epochs"
      arg_type = Int
      default = 10
    "--learning-rate", "-l"
      help = "initial learning rate for the chosen optimizer [-o]"
      arg_type = Float64
      default = 5e-5
    "--noise", "-N"
      help = "magnitude of the applied random noise on the patches (TODO)"
      arg_type = Float64
      default = 0.2
    "--proximity-kernel-size", "-k"
      help = "size of the peaks in the proximity map"
      arg_type = Int
      default = 7
    "--proximity-kernel-height", "-K"
      help = "height of the peaks in the proximity map"
      arg_type = Float64
      default = 100.
    "--optimizer", "-o"
      help = "optimizer used - one of 'adam', 'rmsprop', 'nesterov'"
      arg_type = String
      default = "adam"
    "modelfile"
      required = true
      arg_type = String
      help = "DCellC model (.dccm) file that the trained model is saved to"
    "images"
      nargs='+'
      required = true
      arg_type = String
      help = "image (and corresponding annotation) files that the model trained on"
  end
end

function cmd_train(args)

  # Load labeled images

  lmgs = [ lmgload(imgfile) for imgfile in args["images"] ]

  # Make sure that all images have the same type

  I = typeof(first(lmgs[1]))

  if !all(typeof.(first.(lmgs)) .== I)
    println("Error: all image files must have the same color format")
    exit()
  end

  # Extract some more options

  mtype = args["model-type"]
  otype = args["optimizer"]
  bn    = args["batch-normalization"]

  # Initialize the model

  if mtype == "unetlike"
    model = UNetLike(I, bn=bn)
  elseif mtype == "multiscale3"
    model = Multiscale3(I, bn=bn)
  elseif mtype == "fcrna"
    model = FCRNA(I, bn=bn)
  end

  if otype == "adam"
    opt = Adam
  elseif otype == "rmsprop"
    opt = Rmsprop
  elseif otype == "nesterov"
    opt = Nesterov
  else
    println(STDERR, "Optimizer $(args["optimizer"]) invalid")
  end

  if args["no-gpu"] || gpu() < 0
    println("# CPU mode")
    at = Array{Float32}
  else
    println("# GPU mode")
    at = KnetArray{Float32}
  end

  train!(model, lmgs,
         epochs = args["epochs"],
         modelpath = args["modelfile"],
         patchmode = args["patch-number"],
         patchsize = args["patch-size"],
         batchsize = args["batch-size"],
         peakheight = args["proximity-kernel-height"],
         kernelsize = args["proximity-kernel-size"],
         record = false, at = at,
         lr = args["learning-rate"])
end


function get_density(obj::Atmosphere, arg0::AbsoluteDate, arg1::Vector3D, arg2::Frame)
    return jcall(obj, "getDensity", jdouble, (AbsoluteDate, Vector3D, Frame), arg0, arg1, arg2)
end

function get_density(obj::Atmosphere, arg0::FieldAbsoluteDate, arg1::FieldVector3D, arg2::Frame)
    return jcall(obj, "getDensity", RealFieldElement, (FieldAbsoluteDate, FieldVector3D, Frame), arg0, arg1, arg2)
end

function get_frame(obj::Atmosphere)
    return jcall(obj, "getFrame", Frame, ())
end

function get_velocity(obj::Atmosphere, arg0::AbsoluteDate, arg1::Vector3D, arg2::Frame)
    return jcall(obj, "getVelocity", Vector3D, (AbsoluteDate, Vector3D, Frame), arg0, arg1, arg2)
end

function get_velocity(obj::Atmosphere, arg0::FieldAbsoluteDate, arg1::FieldVector3D, arg2::Frame)
    return jcall(obj, "getVelocity", FieldVector3D, (FieldAbsoluteDate, FieldVector3D, Frame), arg0, arg1, arg2)
end


# Get traces

import Base.Profile: LineInfo

typealias IP UInt
typealias RawData Vector{IP}
typealias Trace Vector{LineInfo}

const lidict = Dict{IP,LineInfo}()
lookup(ip::IP) = haskey(lidict, ip) ? lidict[ip] : (lidict[ip] = Profile.lookup(ip))
lookup(ips::RawData) = map(lookup, ips)

pruneC(trace::Trace) = filter(line->!line.fromC, trace)

traces(data::Vector{UInt}) =
  @>> split(data, 0, keep=false) map(lookup) map!(pruneC) map!(reverse) filter!(t->!isempty(t))

# Tree Implementation

immutable Node{T}
  data::T
  children::Vector{Node{T}}
end

Node{T}(x::T) = Node(x, Node{T}[])
Node{T}(x::T, children::Node{T}...) = Node(x, [children...])

Base.push!(parent::Node, child::Node) = push!(parent.children, child)
isleaf(node::Node) = isempty(node.children)

# Profile Trees

type ProfileNode
  line::LineInfo
  count::Int
end

ProfileNode(line::LineInfo) = ProfileNode(line, 1)

typealias ProfileTree Node{ProfileNode}

tree(trace::Trace) =
  length(trace) ≤ 1 ?
    Node(ProfileNode(trace[1])) :
    Node(ProfileNode(trace[1]), tree(trace[2:end]))

# Conceptually, a trace is a tree with no branches
# We merge trees by (a) increasing the count of the common nodes
# and (b) adding any new nodes as children.
function Base.merge!(node::ProfileTree, trace::Trace)
  @assert !isempty(trace) && node.data.line == trace[1]
  node.data.count += 1
  length(trace) == 1 && return node
  for child in node.children
    if child.data.line == trace[2]
      merge!(child, trace[2:end])
      return node
    end
  end
  push!(node, tree(trace[2:end]))
  return node
end

function tree(traces::Vector{Trace})
  root = Node(ProfileNode(Profile.UNKNOWN))
  traces = map(trace -> [Profile.UNKNOWN, trace...], traces)
  for trace in traces
    merge!(root, trace)
  end
  return root
end

depth(node::Node) =
  isleaf(node) ? 1 : 1 + maximum(map(depth, node.children))

# Remove redundant lines

childwidths(node::ProfileTree) =
  map(child -> child.data.count/node.data.count, node.children)

function trimroot(tree::ProfileTree)
  validchildren = tree.children[childwidths(tree) .> 0.1]
  length(validchildren) == 1 ? trimroot(validchildren[1]) : tree
end

function sortchildren!(tree::ProfileTree)
  sort!(map!(sortchildren!,tree.children), by = node->node.data.line.line)
  tree
end

# Flatten the tree

function addmerge!(a::Associative, b::Associative)
  for (k, v) in b
    a[k] = haskey(a, k) ? a[k]+b[k] : b[k]
  end
  return a
end

flatlines(tree::ProfileTree; total = tree.data.count) =
  reduce(addmerge!,
         [tree.data.line=>tree.data.count/total],
         map(t->flatlines(t, total = total), tree.children))

function fetch()
  data = Profile.fetch()
  isempty(data) && error("You need to do some profiling first.")
  @> data traces tree trimroot sortchildren!
end


using MLStyle

"""
    @yao_str
    yao"..."

The mark up language for quantum circuit.
"""
macro yao_str(str::String)
    yaofromstring(str)
end

function yaofromstring(x::String)
    ex = Meta.parse(x)
    parse_yaoscript(ex)
end

macro yaoscript(ex)
    parse_yaoscript(ex)
end

function parse_yaoscript(ex)
    @match ex begin
        Expr(:let, header, body) => begin
            info = ParseInfo(-1, "")
            if header.head != :block
                header = [header]
            else
                header = header.args
            end
            for hd in header
                @match hd begin
                    :(nqubits = $n) => (info.nbit = Int(n))
                    :(version = $v) => (info.version = String(v))
                    _ => error("unknown configuration $header")
                end
            end
            parse_ex(body |> rmlines, info)
        end
        _ => error("wrong format, expect expression like `let nqubits=5 GATEDEF end`, got $ex")
    end
end

yaofromfile(x::String) = yaofromstring(read(x, String))
parse_ex(ex, nbit::Int) = parse_ex(ex, ParseInfo(nbit, ""))

mutable struct ParseInfo
    nbit::Int
    version::String
end

function parse_param(x)
    @match x begin
        ::Number => x
        :(:π) => x
        _ => error("can not parse parameter $x.")
    end
end

function parse_ex(ex, info::ParseInfo)
    @match ex begin
        :(version = $vnumber) => (info.version = String(vnumber); nothing)
        :(nqubits = $x) => (info.nbit = Int(x); nothing)
        ::Nothing => nothing
        :($g') => :($(parse_ex(g, info))')
        :($a * $b) => :($(Number(a)) * $(parse_ex(b, info)))
        :(kron($(args...))) => :(kron($(parse_ex.(args, Ref(ParseInfo(1, info.version)))...)))
        :(repeat($(exloc...)) => $g) => begin
            loc = render_loc((exloc...,), info.nbit)
            :(repeat($(info.nbit), $(parse_ex(g, ParseInfo(1, info.version))), $loc))
        end
        :(cache($g)) => :(cache($(parse_ex(g, info))))
        :(rot($g, $theta)) => :(rot($(parse_ex(g, info)), $(parse_param(theta))))
        :(time($dt) => $h) => :(time_evolve($(parse_ex(h, info)), $(parse_param(dt))))
        :($exloc => Measure) => parse_ex(:($exloc => Measure(nothing) => nothing), info)
        :($exloc => Measure($op)) => parse_ex(:($exloc => Measure($op) => nothing), info)
        :($exloc => Measure => $post) => parse_ex(:($exloc => Measure(nothing) => $post), info)
        :($exloc => Measure($op) => $post) => begin
            locs = exloc == :ALL ? :(AllLocs()) : render_loc(exloc, info.nbit)
            op =
                op isa Nothing || op == :nothing ? :(ComputationalBasis()) :
                parse_ex(op, exloc == :ALL ? info : ParseInfo(length(locs), info.version))
            @match post begin
                ::Nothing || :nothing => :(Measure($(info.nbit); locs = $locs, operator = $(op)))
                :(resetto($(rbits...))) => begin
                    cb = bit_literal(render_bitstring.(rbits)...)
                    :(Measure($(info.nbit); locs = $locs, operator = $(op), resetto = $cb))
                end
                :remove => :(Measure($(info.nbit); locs = $locs, operator = $(op), remove = true))
            end
        end
        :(+($(args...))) => :(+($(parse_ex.(args, Ref(info))...)))
        :(focus($(exloc...)) => $g) => begin
            loc = render_loc((exloc...,), info.nbit)
            :(subroutine($(info.nbit), $(parse_ex(g, ParseInfo(length(loc), info.version))), $loc))
        end
        :(
            begin
                $(cargs...)
            end
        ) => begin
            args = filter(x -> x !== nothing, [parse_ex(arg, info) for arg in cargs])
            :(chain($(info.nbit), [$(args...)]))
        end
        :($exloc => $gate) => begin
            loc = render_loc(exloc, info.nbit)
            :(put($(info.nbit), $loc => $(parse_ex(gate, ParseInfo(length(loc), info.version)))))
        end
        :($(cargs...), $exloc => $gate) => begin
            loc = render_loc(exloc, info.nbit)
            cbits = render_cloc.(cargs, Ref(info))
            if cbits[1] isa Integer
                :(control(
                    $(info.nbit),
                    $cbits,
                    $loc => $(parse_ex(gate, ParseInfo(length(loc), info.version))),
                ))
            else
                :(kron($(info.nbit), $(cbits...), $loc => $(parse_ex(gate, ParseInfo(1, info.version)))))
            end
        end
        :($f($(args...))) => gate_expr(Val(Symbol(f)), args, info)

        ::LineNumberNode => nothing
        ::Symbol => ex  # const gate
        _ => error("scipt format error! got $ex of type $(typeof(ex))")
    end
end

function check_dumpload(gate::AbstractBlock{N}) where {N}
    gate2 = eval(parse_ex(dump_gate(gate), N))
    gate2 == gate || mat(gate2) ≈ mat(gate)
end

render_bitstring(ex) = @match ex begin
    ::Number => begin
        if ex == 1 || ex == 0
            ex
        else
            error("expect a bitstring like `1` or `(1,0)`, got $ex")
        end
    end
    ::Tuple => render_bitstring.(ex)
    :($(args...),) => (render_bitstring.(args)...,)
    _ => error("expect a bitstring like `1` or `(1,0)`, got $ex")
end

render_loc(ex, nbit::Int) = @match ex begin
    :($(args...),) => (render_loc.(args, nbit)...,)
    ::Number => Int(ex)
    :($a:$b) => Int(a):Int(b)
    :ALL => (1:nbit...,)
    :($a:$step:$b) => Int(a):Int(step):Int(b)
    ::Tuple => Int.(ex)
    ::UnitRange{Int} => ex
    _ => error("expect a location specification like `2`, `2:5` or `(2,3)`, got $ex")
end

render_cloc(ex, info) = @match ex begin
    :($a => C($b)) => begin
        if !(b == 1 || b == 0)
            error("expect a control values `0` or `1`, got $ex")
        end
        Int(a) * (2 * Int(b) - 1)
    end
    :($a => C) => render_cloc(:($a => C(1)), info)
    :($a => $g) => :($(render_loc(a, info.nbit)) => $(parse_ex(g, ParseInfo(1, info.version))))
    _ => error("expect a control location specification like `2=>0` or `3=>1`, got $ex")
end

"""
    gate_expr(::Val{G}, args, info)

Obtain the gate constructior from its YaoScript expression.
`G` is a symbol for the gate type,
the default constructor is `G(args...)`.
`info` contains the informations about the number of qubit and Yao version.
"""
function gate_expr(::Val{G}, args, info) where {G}
    throw(NotImplementedError(:gate_expr, (Val(G), args, info)))
end

render_arg(ex, info) = @match ex begin
    # locs
    :($(args...),) => (render_loc.(args, info.nbit)...,)
    ::Integer => Int(ex)
    :($a:$b) => Int(a):Int(b)
    :ALL => (1:nbit...,)
    :($a:$step:$b) => Int(a):Int(step):Int(b)
    ::Tuple => Int.(ex)

    # floating point numbers
    ::Number => Number(ex)

    # Pair of (loc => gate)
    :($a => $g) => begin
        locs = render_loc(a, info.nbit)
        :($(locs) => $(parse_ex(g, ParseInfo(length(locs), info.version))))
    end

    # try parse a gate
    _ => parse_ex(ex, info)
end

for (G, F) in [(:ShiftGate, :shift), (:PhaseGate, :phase)]
    fname = QuoteNode(F)
    @eval function dump_gate(blk::$G)
        Expr(:call, $fname, tokenize_param(blk.theta))
    end
    @eval function gate_expr(::Val{$(QuoteNode(F))}, args, info)
        Expr(:call, $fname, parse_param(args[1]))
    end
end


module test_h3_api_traversal

using Test
using H3.API # kRing maxKringSize kRingDistances hexRange hexRanges h3Line h3LineSize h3Distance experimentalH3ToLocalIj CoordIJ experimentalLocalIjToH3

@test kRing(0x085283473fffffff, 0) == [0x085283473fffffff]
@test kRing(0x085283473fffffff, 1) == [0x085283473fffffff, 0x085283447fffffff, 0x08528347bfffffff, 0x085283463fffffff, 0x085283477fffffff, 0x08528340ffffffff, 0x08528340bfffffff]

@test maxKringSize(0) == 1
@test maxKringSize(1) == 7

@test kRingDistances(0x08928308280fffff, 0) == (out = [0x08928308280fffff], distances = [0])
@test kRingDistances(0x08928308280fffff, 1) == (out = [0x08928308280fffff, 0x08928308280bffff, 0x089283082873ffff, 0x089283082877ffff, 0x08928308283bffff, 0x089283082807ffff, 0x089283082803ffff], distances = [0, 1, 1, 1, 1, 1, 1])

@test hexRange(0x085283473fffffff, 0) == [0x085283473fffffff]
@test hexRange(0x085283473fffffff, 1) == [0x085283473fffffff, 0x085283447fffffff, 0x08528347bfffffff, 0x085283463fffffff, 0x085283477fffffff, 0x08528340ffffffff, 0x08528340bfffffff]

@test hexRangeDistances(0x08928308280fffff, 0) == (out = [0x08928308280fffff], distances = [0])
@test hexRangeDistances(0x08928308280fffff, 1) == (out = [0x08928308280fffff, 0x08928308280bffff, 0x089283082873ffff, 0x089283082877ffff, 0x08928308283bffff, 0x089283082807ffff, 0x089283082803ffff], distances = [0, 1, 1, 1, 1, 1, 1])

@test hexRanges([0x08928308280fffff], 0) == [0x08928308280fffff]
@test hexRanges([0x08928308280fffff], 1) == [0x08928308280fffff, 0x08928308280bffff, 0x089283082873ffff, 0x089283082877ffff, 0x08928308283bffff, 0x089283082807ffff, 0x089283082803ffff]

@test hexRing(0x08928308280fffff, 0) == []
@test hexRing(0x08928308280fffff, 1) == [0x089283082803ffff, 0x08928308280bffff, 0x089283082873ffff, 0x089283082877ffff, 0x08928308283bffff, 0x089283082807ffff]

@test h3Line(0x08928308280fffff, 0x08928308280fffff) == [0x08928308280fffff]
@test h3Line(0x08928308280fffff, 0x08928308283bffff) == [0x08928308280fffff, 0x08928308283bffff]

@test h3LineSize(0x08928308280fffff, 0x08928308280fffff) == 1
@test h3LineSize(0x08928308280fffff, 0x08928308283bffff) == 2

@test h3Distance(0x08928308280fffff, 0x08928308280fffff) == 0
@test h3Distance(0x08928308280fffff, 0x08928308283bffff) == 1

@test experimentalH3ToLocalIj(0x08828308281fffff, 0x0882830828dfffff) == CoordIJ(393, 337)
@test experimentalLocalIjToH3(0x08828308281fffff, CoordIJ(393, 337)) == 0x0882830828dfffff

end # module test_h3_api_traversal


"""
    IEEE754-2019

Provide performant, conforming implementations of functions recommended within the 2019 revision of IEEE754.
"""
module IEEE754-2019

end  # IEEE754-2019


###############################################################################
#
#   Float.jl : Additional Nemo functionality for Julia AbstractFloat types
#
###############################################################################

###############################################################################
#
#   Data type and parent object methods
#
###############################################################################

JuliaRealField = Floats{BigFloat}()

RDF = Floats{Float64}()

parent(a::T) where T <: AbstractFloat = Floats{T}()

elem_type(::Type{Floats{T}}) where T <: AbstractFloat = T

parent_type(::Type{T}) where T <: AbstractFloat = Floats{T}

base_ring(a::AbstractFloat) = Union{}

isdomain_type(::Type{T}) where T <: AbstractFloat = true

isexact_type(::Type{T}) where T <: AbstractFloat = false

###############################################################################
#
#   Basic manipulation
#
###############################################################################

zero(::Floats{T}) where T <: AbstractFloat = T(0)

one(::Floats{T}) where T <: AbstractFloat = T(1)

if VERSION < v"0.7.0-DEV.1144"
isone(a::AbstractFloat) = a == 1
end

isunit(a::AbstractFloat) = a != 0

canonical_unit(a::AbstractFloat) = a

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

function show(io::IO, R::Floats)
   print(io, "Floats")
end

needs_parentheses(::AbstractFloat) = false

isnegative(a::AbstractFloat) = a < 0

show_minus_one(::Type{T}) where T <: AbstractFloat = false

###############################################################################
#
#   Exact division
#
###############################################################################

divexact(a::T, b::T) where T <: AbstractFloat = a/b

divexact(a::AbstractFloat, b::Union{Signed, Unsigned}) = a/b

divexact(a::BigFloat, b::BigInt) = a/b

divexact(a::Union{Signed, Unsigned}, b::AbstractFloat) = a/b

divexact(a::BigInt, b::BigFloat) = a/b

divexact(a::AbstractFloat, b::Rational{T}) where T <: Union{Signed, Unsigned} = a/b

divexact(a::BigFloat, b::Rational{BigInt}) = a/b

divexact(a::Rational{T}, b::AbstractFloat) where T <: Union{Signed, Unsigned} = a/b

divexact(a::Rational{BigInt}, b::BigFloat) = a/b

###############################################################################
#
#   GCD
#
###############################################################################

function gcd(a::T, b::T) where T <: AbstractFloat
   if a == 0 && b == 0
      return T(0)
   else
      return T(1)
   end
end

###############################################################################
#
#   Unsafe functions
#
###############################################################################

function zero!(a::AbstractFloat)
   return 0
end

function zero!(a::BigFloat)
   ccall((:mpfr_set_si, :libmpfr), Void,
         (Ref{BigFloat}, Int, Int32), a, 0, Base.MPFR.ROUNDING_MODE[])
   return a
end

function mul!(a::T, b::T, c::T) where T <: AbstractFloat
   return b*c
end

function mul!(a::BigFloat, b::BigFloat, c::BigFloat)
   ccall((:mpfr_mul, :libmpfr), Void,
         (Ref{BigFloat}, Ref{BigFloat}, Ref{BigFloat}, Int32),
                 a, b, c, Base.MPFR.ROUNDING_MODE[])
   return a
end

function add!(a::T, b::T, c::T) where T <: AbstractFloat
   return b + c
end

function add!(a::BigFloat, b::BigFloat, c::BigFloat)
   ccall((:mpfr_add, :libmpfr), Void,
         (Ref{BigFloat}, Ref{BigFloat}, Ref{BigFloat}, Int32),
                 a, b, c, Base.MPFR.ROUNDING_MODE[])
   return a
end

function addeq!(a::T, b::T) where T <: AbstractFloat
   return a + b
end

function addeq!(a::BigFloat, b::BigFloat)
   ccall((:mpfr_add, :libmpfr), Void,
         (Ref{BigFloat}, Ref{BigFloat}, Ref{BigFloat}, Int32),
                 a, a, b, Base.MPFR.ROUNDING_MODE[])
   return a
end

function addmul!(a::T, b::T, c::T, d::T) where T <: AbstractFloat
   return a + b*c
end

function addmul!(a::BigFloat, b::BigFloat, c::BigFloat, d::BigFloat)
   ccall((:mpfr_fma, :libmpfr), Void,
         (Ref{BigFloat}, Ref{BigFloat}, Ref{BigFloat}, Ref{BigFloat}, Int32),
                 a, b, c, a, Base.MPFR.ROUNDING_MODE[])
   return a
end

function addmul!(a::T, b::T, c::T) where T <: AbstractFloat # special case, no temporary required
   return a + b*c
end

function addmul!(a::BigFloat, b::BigFloat, c::BigFloat) # special case, no temporary required
   ccall((:mpfr_fma, :libmpfr), Void,
         (Ref{BigFloat}, Ref{BigFloat}, Ref{BigFloat}, Ref{BigFloat}, Int32),
                 a, b, c, a, Base.MPFR.ROUNDING_MODE[])
   return a
end

###############################################################################
#
#   Random generation
#
###############################################################################

function rand(R::Floats, n::UnitRange{AbstractFloat})
   return R(n.start + rand(Float64)*(n.stop - n.start))
end

function rand(R::Floats, n::UnitRange{Int})
   return R(n.start + rand(Float64)*(n.stop - n.start))
end

###############################################################################
#
#   Parent object call overload
#
###############################################################################

function (a::Floats{T})() where T <: AbstractFloat
   return T(0)
end

function (a::Floats{T})(b::AbstractFloat) where T <: AbstractFloat
   return T(b)
end

function (a::Floats{T})(b::Union{Signed, Unsigned}) where T <: AbstractFloat
   return T(b)
end

function (a::Floats{BigFloat})(b::BigInt)
   return BigFloat(b)
end

function (a::Floats{T})(b::Rational{S}) where {S <: Union{Signed, Unsigned}, T <: AbstractFloat}
   return T(b)
end

function (a::Floats{BigFloat})(b::Rational{BigInt})
   return BigFloat(b)
end


location="Belgium"
df = DataFrame(XLSX.readtable("/Users/shardy/Documents/GitHub/hv_offshore_topology_optimization/v1.0/layout/input_data/"*location*"/data.xlsx", "wind_data")...)
wnd="Norther"
cbl_data=get_220kV_cables()
cable_tables=Array{Array{cable,1},1}()
km=72.5
#ps=[50.0,100.0,150.0,200.0,250.0,300.0,350.0,400.0,450.0,500.0,550.0,600.0,650.0,700.0,750.0,800.0,850.0,900.0,950.0,1000.0,1050.0,1100.0,1150.0,1200.0,1250.0,1300.0,1350.0,1400.0,1450.0,1500.0]
#ps=[50.0,100.0,150.0,200.0,250.0,300.0,350.0,400.0,450.0,500.0,550.0,600.0,650.0,700.0]
#
ps=[1200.0]

osss=Vector{}()
cbls=Vector{}()
osss_costs=Vector{}()
cbls_costs=Vector{}()
ks=get_Cost_Data()
xfo_data=get_Xfo_Data()
xfo_on=transformer()
for p in ps
    oss=bus()
    xfo_off=transformer()
    xfo_off.relia=get_offshore_xfo_failure_data(xfo_off)
    xfo_off.wnd=wnd
    xfo_off.mva=p
    xfo_on=deepcopy(xfo_off)
    xfo_off=xfo_oss(xfo_off,ks,xfo_data)
    push!(oss.xfmrs,xfo_off)
    xfo_pcc(xfo_on,ks,xfo_data)

    plat=platform()
    plat.mva=p
    plat.wnd=wnd
    plat=cost_ac_platform(plat,ks)
    plat=adjust_base_ac_platform(plat,ks)
    push!(oss.plat,plat)
    push!(osss,oss)

cable()

    push!(cable_tables,solve_cable_interval((1,km,p),cbl_data,wnd,ks))
    cable_dictionary=make_dictionaries_cables([cable_tables])
    sg_qc=cable_dictionary["220.0"][string(p)][length(cable_dictionary["220.0"][string(p)])].costs.sg+cable_dictionary["220.0"][string(p)][length(cable_dictionary["220.0"][string(p)])].costs.qc
    cbl_cost=cable_dictionary["220.0"][string(p)][length(cable_dictionary["220.0"][string(p)])].costs.ttl-sg_qc-cable_dictionary["220.0"][string(p)][length(cable_dictionary["220.0"][string(p)])].costs.rlc-cable_dictionary["220.0"][string(p)][length(cable_dictionary["220.0"][string(p)])].costs.eens-cable_dictionary["220.0"][string(p)][length(cable_dictionary["220.0"][string(p)])].costs.cm
    push!(cbls,cable_dictionary["220.0"][string(p)][length(cable_dictionary["220.0"][string(p)])])
    println(sg_qc)
    println(sg_qc)
    println(sg_qc)
    push!(cbls_costs,cbl_cost/km)
    push!(osss_costs,(plat.costs.ttl-plat.costs.cm+xfo_off.costs.ttl-xfo_off.costs.cm-xfo_off.costs.eens-xfo_off.costs.tlc+sg_qc/2))
    push!(osss_costs,(plat.costs.ttl-plat.costs.cm+sg_qc/2))
    #push!(osss_costs,xfo_on.costs.ttl+sg_qc/2)
end

cbls_costs[1]*km

println(osss_costs)

plotly()
p=plot()
plot!(p,[ps],[cbls_costs],xticks = 0:200:800,xlims=(0,800),ylims=(0,3),color = :black,label="",xaxis = ("MW", font(20, "Courier")),yaxis = ("ME", font(20, "Courier")))
gui()=#

plotly()
p=plot()
plot!(p,[ps],[osss_costs],xticks = 0:100:800,xlims=(0,700),ylims=(0,250),color = :black,label="",xaxis = ("MW", font(20, "Courier")),yaxis = ("ME", font(20, "Courier")))
gui()

osss_costs_orig=deepcopy(osss_costs)

GMIN=minimum([wf.mva for wf in owpps])
A=range(1,stop=30)
base_b=zeros(Int8,1,length(owpps))
B=Array{Tuple{Array{Int8,2},Int64},1}()
combos=collect(Iterators.flatten([combinations(A, k) for k = 1:9]))


using SolidStateDetectors.ConstructiveSolidGeometry: CSGUnion, CSGIntersection, CSGDifference, CSG_dict,
        parse_r_of_primitive, parse_height_of_primitive, parse_translate_vector, parse_rotation_matrix
using SolidStateDetectors: construct_units
import SolidStateDetectors.ConstructiveSolidGeometry: Geometry, Dictionary
using SolidStateDetectors.ConstructiveSolidGeometry: AbstractGeometry, Transformations,
    CSGUnion, CSGDifference, CSGIntersection, Cone, CartesianVector, HexagonalPrism
using DataStructures: OrderedDict
using IntervalSets
using Rotations
using StaticArrays
using Unitful
using JSON
using YAML

#old implementation of Constructive Solid Geometries:
function Geometry(::Type{T}, t::Type{CSGUnion}, dict::Union{Dict, OrderedDict}, input_units::NamedTuple, transformations::Transformations{T}) where {T}
    @assert haskey(dict, "parts") "Please specify 'parts' of the '$(dict["type"])'."
    sum( map(x-> Geometry(T, x, input_units, transformations), dict["parts"]) )
end

function Geometry(::Type{T}, ::Type{CSGIntersection}, dict::Union{Dict, OrderedDict}, input_units::NamedTuple, transformations::Transformations{T}) where {T}
    @assert haskey(dict, "parts") "Please specify 'parts' of the '$(dict["type"])'."
    parts = map(x-> Geometry(T, x, input_units, transformations), dict["parts"])
    reduce(&, parts)
end

function Geometry(::Type{T}, ::Type{CSGDifference}, dict::Union{Dict, OrderedDict}, input_units::NamedTuple, transformations::Transformations{T}) where {T}
    @assert haskey(dict, "parts") "Please specify 'parts' of the '$(dict["type"])'."
    Geometry(T, dict["parts"][1], input_units, transformations) - sum( map(x-> Geometry(T, x, input_units, transformations), dict["parts"][2:end]) )
end

function Geometry(::Type{T}, dict::Union{Dict, OrderedDict}, input_units::NamedTuple, transformations::Transformations{T}) where {T}
    if haskey(dict, "translate")
        length_unit = input_units.length
        t::CartesianVector{T} = parse_translate_vector(T, dict["translate"], length_unit)
        gdict::Dict{Any,Any} = filter(p -> first(p) != "translate", dict)
        return Geometry(T, gdict, input_units, transformations) + t
    end
    Geometry(T, CSG_dict[dict["type"]], dict, input_units, transformations)
end

function Geometry(::Type{T}, filename::String, input_units::NamedTuple, transformations::Transformations{T}) where {T}
    @assert isfile(filename) "The given filename '$(filename)' does not lead to a valid file."
    dict = if endswith(filename, ".json")
        JSON.parsefile(filename)
    elseif endswith(filename, ".yaml")
        YAML.load_file(filename)
    else
        @error "Only JSON and YAML formats are supported at the moment."
    end
    Geometry(T, dict, input_units, transformations::Transformations{T})
end

# UPDATE DUE TO BREAKING CHANGES TO TUBE/CONE: CENTERED AROUND ZERO
# AND REDEFINITION OF HEXAGONALPRISM
function update_primitives!(dict::AbstractDict, input_units::NamedTuple)
    dict_keys = keys(dict)
    #Translate all Tubes and Cone that are defined via "h" and where h is not zero
    if "type" in dict_keys && dict["type"] == "HexagonalPrism"
        rotZ = haskey(dict, "rotZ") ? dict["rotZ"] : 0
        rotZ -= input_units.angle == u"°" ? 30 : π/6
        rotZ = string(rotZ * input_units.angle)
        delete!(dict, "rotZ")
        if rotZ != 0 dict["rotate"] = Dict("Z" => rotZ) end
    elseif "type" in dict_keys && dict["type"] in ["tube", "cone"] && "h" in dict_keys && dict["h"] != 0
        #add translate vector with z=0 if it does not exist
        if !("translate" in dict_keys) dict["translate"] = Dict{String,Any}("z" => 0) end
        dict["translate"]["z"] = dict["translate"]["z"] + Float64(dict["h"] / 2)
        if dict["translate"]["z"] == 0 delete!(dict["translate"], "z") end
        if length(keys(dict["translate"])) == 0 delete!(dict, "translate") end
    end
    for k in dict_keys
        if typeof(dict[k]) <: AbstractDict
            dict[k] = update_primitives!(dict[k], input_units)
        elseif typeof(dict[k]) <: Array
            for i in eachindex(dict[k])
                if typeof(dict[k][i]) <: AbstractDict
                    dict[k][i] = update_primitives!(dict[k][i], input_units)
                end
            end
        end
    end
    dict
end

function update_geometry!(g::AbstractDict, T::DataType, input_units = (length = NoUnits, angle = NoUnits))
    transformations::Transformations{T} = (rotation = one(SMatrix{3, 3, T, 9}), translation = zero(CartesianVector{T}))
    g["geometry"] = Dictionary(Geometry(T, g["geometry"], input_units, transformations))
    return g
end

function restructure_config_file_dict!(config_file_dict::AbstractDict, T::DataType = Float64)

    input_units = construct_units(config_file_dict)
    update_primitives!(config_file_dict, input_units)
    update_units = (length = NoUnits, angle = input_units.angle)

    t = broadcast(obj -> obj["type"], config_file_dict["objects"])

    semiconductors = config_file_dict["objects"][findall(t .== "semiconductor")]
    @assert length(semiconductors) == 1 "Config files need exactly one semiconductor per detector. This one has $(length(semiconductors)) semiconductors."
    semiconductor = semiconductors[1]

    semiconductor = begin
        delete!(semiconductor, "type")
        if "charge_density_model" in keys(semiconductor)
            semiconductor["impurity_density"] = semiconductor["charge_density_model"]
            delete!(semiconductor, "charge_density_model")
        end
        update_geometry!(semiconductor, T, update_units)
        semiconductor
    end

    contacts = [
        begin
        delete!(contact, "type")
        if "channel" in keys(contact)
            contact["id"] = contact["channel"]
            delete!(contact, "channel")
        end
        update_geometry!(contact, T, update_units)
        contact
        end
        for contact in config_file_dict["objects"][findall(t .== "contact")]
    ]

    passives = [
        begin
        delete!(passive, "type")
        update_geometry!(passive, T, update_units)
        passive
        end
        for passive in config_file_dict["objects"][findall(t .== "passive")]
    ]

    virtual_drift_volumes = [
        begin
        delete!(virtual_volume, "type")
        update_geometry!(virtual_volume, T, update_units)
        virtual_volume
        end
        for virtual_volume in config_file_dict["objects"][findall(t .== "virtual_drift_volume")]
    ]

    config_file_dict["detectors"] = []
    push!(config_file_dict["detectors"], OrderedDict{String, Any}("semiconductor" => semiconductor) )
    if length(contacts) > 0 config_file_dict["detectors"][1]["contacts"] = contacts end
    if length(virtual_drift_volumes) > 0 config_file_dict["detectors"][1]["virtual_drift_volumes"] = virtual_drift_volumes end
    if length(passives) > 0 config_file_dict["surroundings"] = passives end

    delete!(config_file_dict, "objects")
    config_file_dict

end

"""
    update_config_file(filename::String)::String

Updates the format of config files from v0.5.3 to v0.6.
Currently supported formats for the config file: .json, .yaml.
Returns the new filename (or an empty string if file could not be converted).
"""
function update_config_file(filename::String)::String
    @assert isfile(filename) "'$(filename)' does not exist!"

    if endswith(filename, ".json")
        data = JSON.parsefile(filename, dicttype = OrderedDict)
        if !haskey(data, "objects")
            @warn "The configuration file does not have the key 'objects'.\n"*
                  "Assuming that this configuration file has already been updated. Skipping..."
            return ""
        end
        restructure_config_file_dict!(data)
        new_filename = replace(filename, ".json" => "_updated.json")
        open(new_filename,"w") do f
            JSON.print(f, data, 4)
        end
    elseif endswith(filename, ".yaml")
        data = YAML.load_file(filename, dicttype = OrderedDict)
        if !haskey(data, "objects")
            @warn "The configuration file does not have the key 'objects'.\n"*
                  "Assuming that this configuration file has already been updated. Skipping..."
            return ""
        end
        restructure_config_file_dict!(data)
        new_filename = replace(filename, ".yaml" => "_updated.yaml")
        YAML.write_file(new_filename, data)
    else
        error("Currently only .json and .yaml files are supported.")
        new_filename = ""
    end
    new_filename
end


module TTTexample
import Flux

using AlphaZero
using ..MuZero

using ParameterSchedulers: Scheduler, Cos

gspec = Examples.games["tictactoe"]

n=16
self_play = (;
  sim=SimParams(
    num_games=100,
    num_workers=n,
    batch_size=8,
    use_gpu=false, # not used, change device to Flux.gpu
    # todevice=Flux.cpu, #?
    reset_every=4, #not used, mcts resets everytime
    flip_probability=0.,
    alternate_colors=false),
  mcts = MctsParams(
    num_iters_per_turn=64, #1000 benchmark
    cpuct=1.25,
    temperature=ConstSchedule(1.0),
    dirichlet_noise_ϵ=0.25,
    dirichlet_noise_α=0.1),
    device=Flux.cpu) # Flux.cpu || Flux.gpu

arena = (;
  sim=SimParams(
    num_games=50,
    num_workers=n,
    batch_size=n÷2,
    use_gpu=false, # not used, change device to Flux.gpu
    reset_every=1,
    flip_probability=0., #0.5
    alternate_colors=true),
  mcts = MctsParams(
    self_play.mcts,
    temperature=ConstSchedule(0.3),
    dirichlet_noise_ϵ=0.1),
  update_threshold=0.0,
  device=Flux.cpu)# Flux.cpu || Flux.gpu

learning_params = (;
  num_unroll_steps=5, #if =0, g is not learning, muzero-general=20
  td_steps=20, # with max length=9, always go till the end of the game, rootvalues don't count
  discount=0.997,
  #// value_loss_weight = 0.25, #TODO
  l2_regularization=1f-4, #Float32
  #// l2_regularization=0f0, #Float32
  loss_computation_batch_size=512,
  batches_per_checkpoint=10,
  num_checkpoints=1,
  opt=Scheduler(
    Cos(λ0=3e-3, λ1=1e-5, period=10000), # cosine annealing, google period 2e4, generat doesn't use any
    Flux.ADAM()
  ),
  model_type = :mlp, # :mlp || :resnet
  device=Flux.cpu # Flux.cpu || Flux.gpu
)

benchmark_sim = SimParams(
    num_games=400,
    num_workers=n,
    batch_size=n÷2,
    use_gpu=false,
    reset_every=1,
    flip_probability=0.0, #0.5
    alternate_colors=true)

bench_mcts = MctsParams(
  num_iters_per_turn=400, #1000 benchmark
  cpuct=1.25,
  temperature=ConstSchedule(0.3),
  dirichlet_noise_ϵ=0.25,
  dirichlet_noise_α=0.1)

benchmark = (;
  vanilla_mcts=Benchmark.Duel(
    Mu(arena.mcts),
    Benchmark.MctsRollouts(bench_mcts),
    benchmark_sim),
  minmax_d5=Benchmark.Duel(
    Mu(arena.mcts),
    Benchmark.MinMaxTS(depth=5, amplify_rewards=true, τ=1.),
    benchmark_sim)
)

  μparams = MuParams(self_play, learning_params, arena, 5_000, 3000)

# muzero-general params
# μNetworkHP = MuNetworkHP(
#   gspec,
#   PredictionHP(hiddenstate_shape=32, width=64, depth_common=-1,
#     depth_vectorhead=0, depth_scalarhead=0, use_batch_norm=false, batch_norm_momentum=1.),
#   DynamicsHP(hiddenstate_shape=32, width=64, depth_common=-1,
#    depth_vectorhead=0, depth_scalarhead=0s,  use_batch_norm=false, batch_norm_momentum=1.),
#   RepresentationHP(hiddenstate_shape=32, width=0, depth=-1))

  hs_shape = 32
  simμNetworkHP = MuNetworkHP(
    gspec,
    PredictionSimpleHP(hiddenstate_shape=hs_shape, width=64, depth_common=-1,
      depth_vectorhead=0, depth_scalarhead=0, use_batch_norm=false, batch_norm_momentum=0.8f0),
    DynamicsSimpleHP(hiddenstate_shape=hs_shape, width=64, depth_common=-1,
     depth_vectorhead=0, depth_scalarhead=0,  use_batch_norm=false, batch_norm_momentum=0.8f0),
    RepresentationSimpleHP(hiddenstate_shape=hs_shape, width=0, depth=-1))

  env = MuEnv(gspec, μparams, MuNetwork(simμNetworkHP))

end # module