module Atmos

export AtmosModel, AtmosAcousticLinearModel, AtmosAcousticGravityLinearModel

using CLIMAParameters
using CLIMAParameters.Planet: grav, cp_d
using CLIMAParameters.Atmos.SubgridScale: C_smag
using DocStringExtensions
using LinearAlgebra, StaticArrays
using ..ConfigTypes
using ..Orientations
import ..Orientations:
    vertical_unit_vector,
    altitude,
    latitude,
    longitude,
    projection_normal,
    gravitational_potential,
    ∇gravitational_potential,
    projection_tangential

using ..VariableTemplates
using ..Thermodynamics
using ..TemperatureProfiles

using ..TurbulenceClosures
import ..TurbulenceClosures: turbulence_tensors
using ..TurbulenceConvection

import ..Thermodynamics: internal_energy
using ..MPIStateArrays: MPIStateArray
using ..Mesh.Grids:
    VerticalDirection,
    HorizontalDirection,
    min_node_distance,
    EveryDirection,
    Direction

using ClimateMachine.BalanceLaws
using ClimateMachine.Problems

import ClimateMachine.BalanceLaws:
    vars_state,
    flux_first_order!,
    flux_second_order!,
    source!,
    wavespeed,
    boundary_state!,
    compute_gradient_argument!,
    compute_gradient_flux!,
    transform_post_gradient_laplacian!,
    init_state_auxiliary!,
    init_state_prognostic!,
    update_auxiliary_state!,
    indefinite_stack_integral!,
    reverse_indefinite_stack_integral!,
    integral_load_auxiliary_state!,
    integral_set_auxiliary_state!,
    reverse_integral_load_auxiliary_state!,
    reverse_integral_set_auxiliary_state!

import ClimateMachine.DGMethods:
    LocalGeometry, lengthscale, resolutionmetric, DGModel

import ..DGMethods.NumericalFluxes:
    boundary_state!,
    boundary_flux_second_order!,
    normal_boundary_flux_second_order!,
    NumericalFluxFirstOrder,
    NumericalFluxGradient,
    NumericalFluxSecondOrder,
    CentralNumericalFluxHigherOrder,
    CentralNumericalFluxDivergence,
    CentralNumericalFluxFirstOrder,
    numerical_flux_first_order!,
    NumericalFluxFirstOrder
using ..DGMethods.NumericalFluxes:
    RoeNumericalFlux, HLLCNumericalFlux, RusanovNumericalFlux

import ..Courant: advective_courant, nondiffusive_courant, diffusive_courant

"""
    AtmosModel <: BalanceLaw

A `BalanceLaw` for atmosphere modeling. Users may over-ride prescribed
default values for each field.

# Usage

    AtmosModel(
        param_set,
        problem,
        orientation,
        ref_state,
        turbulence,
        hyperdiffusion,
        spongelayer,
        moisture,
        radiation,
        source,
        tracers,
        data_config,
    )

# Fields
$(DocStringExtensions.FIELDS)
"""
struct AtmosModel{FT, PS, PR, O, RS, T, TC, HD, VS, M, P, R, S, TR, DC} <:
       BalanceLaw
    "Parameter Set (type to dispatch on, e.g., planet parameters. See CLIMAParameters.jl package)"
    param_set::PS
    "Problem (initial and boundary conditions)"
    problem::PR
    "An orientation model"
    orientation::O
    "Reference State (For initial conditions, or for linearisation when using implicit solvers)"
    ref_state::RS
    "Turbulence Closure (Equations for dynamics of under-resolved turbulent flows)"
    turbulence::T
    "Turbulence Convection Closure (e.g., EDMF)"
    turbconv::TC
    "Hyperdiffusion Model (Equations for dynamics of high-order spatial wave attenuation)"
    hyperdiffusion::HD
    "Viscous sponge layers"
    viscoussponge::VS
    "Moisture Model (Equations for dynamics of moist variables)"
    moisture::M
    "Precipitation Model (Equations for dynamics of precipitating species)"
    precipitation::P
    "Radiation Model (Equations for radiative fluxes)"
    radiation::R
    "Source Terms (Problem specific source terms)"
    source::S
    "Tracer Terms (Equations for dynamics of active and passive tracers)"
    tracers::TR
    "Data Configuration (Helper field for experiment configuration)"
    data_config::DC
end

"""
    AtmosModel{FT}()

Constructor for `AtmosModel` (where `AtmosModel <: BalanceLaw`) for LES
and single stack configurations.
"""
function AtmosModel{FT}(
    ::Union{Type{AtmosLESConfigType}, Type{SingleStackConfigType}},
    param_set::AbstractParameterSet;
    init_state_prognostic::ISP = nothing,
    problem::PR = AtmosProblem(init_state_prognostic = init_state_prognostic),
    orientation::O = FlatOrientation(),
    ref_state::RS = HydrostaticState(DecayingTemperatureProfile{FT}(param_set),),
    turbulence::T = SmagorinskyLilly{FT}(0.21),
    turbconv::TC = NoTurbConv(),
    hyperdiffusion::HD = NoHyperDiffusion(),
    viscoussponge::VS = NoViscousSponge(),
    moisture::M = EquilMoist{FT}(),
    precipitation::P = NoPrecipitation(),
    radiation::R = NoRadiation(),
    source::S = (
        Gravity(),
        Coriolis(),
        GeostrophicForcing{FT}(7.62e-5, 0, 0),
        turbconv_sources(turbconv)...,
    ),
    tracers::TR = NoTracers(),
    data_config::DC = nothing,
) where {FT <: AbstractFloat, ISP, PR, O, RS, T, TC, HD, VS, M, P, R, S, TR, DC}

    atmos = (
        param_set,
        problem,
        orientation,
        ref_state,
        turbulence,
        turbconv,
        hyperdiffusion,
        viscoussponge,
        moisture,
        precipitation,
        radiation,
        source,
        tracers,
        data_config,
    )

    return AtmosModel{FT, typeof.(atmos)...}(atmos...)
end

"""
    AtmosModel{FT}()

Constructor for `AtmosModel` (where `AtmosModel <: BalanceLaw`) for GCM
configurations.
"""
function AtmosModel{FT}(
    ::Type{AtmosGCMConfigType},
    param_set::AbstractParameterSet;
    init_state_prognostic::ISP = nothing,
    problem::PR = AtmosProblem(init_state_prognostic = init_state_prognostic),
    orientation::O = SphericalOrientation(),
    ref_state::RS = HydrostaticState(DecayingTemperatureProfile{FT}(param_set),),
    turbulence::T = SmagorinskyLilly{FT}(C_smag(param_set)),
    turbconv::TC = NoTurbConv(),
    hyperdiffusion::HD = NoHyperDiffusion(),
    viscoussponge::VS = NoViscousSponge(),
    moisture::M = EquilMoist{FT}(),
    precipitation::P = NoPrecipitation(),
    radiation::R = NoRadiation(),
    source::S = (Gravity(), Coriolis(), turbconv_sources(turbconv)...),
    tracers::TR = NoTracers(),
    data_config::DC = nothing,
) where {FT <: AbstractFloat, ISP, PR, O, RS, T, TC, HD, VS, M, P, R, S, TR, DC}

    atmos = (
        param_set,
        problem,
        orientation,
        ref_state,
        turbulence,
        turbconv,
        hyperdiffusion,
        viscoussponge,
        moisture,
        precipitation,
        radiation,
        source,
        tracers,
        data_config,
    )

    return AtmosModel{FT, typeof.(atmos)...}(atmos...)
end

"""
    vars_state(m::AtmosModel, ::Prognostic, FT)

Conserved state variables (prognostic variables).
"""
function vars_state(m::AtmosModel, st::Prognostic, FT)
    @vars begin
        ρ::FT
        ρu::SVector{3, FT}
        ρe::FT
        turbulence::vars_state(m.turbulence, st, FT)
        turbconv::vars_state(m.turbconv, st, FT)
        hyperdiffusion::vars_state(m.hyperdiffusion, st, FT)
        moisture::vars_state(m.moisture, st, FT)
        radiation::vars_state(m.radiation, st, FT)
        tracers::vars_state(m.tracers, st, FT)
    end
end

"""
    vars_state(m::AtmosModel, ::Gradient, FT)

Pre-transform gradient variables.
"""
function vars_state(m::AtmosModel, st::Gradient, FT)
    @vars begin
        u::SVector{3, FT}
        h_tot::FT
        turbulence::vars_state(m.turbulence, st, FT)
        turbconv::vars_state(m.turbconv, st, FT)
        hyperdiffusion::vars_state(m.hyperdiffusion, st, FT)
        moisture::vars_state(m.moisture, st, FT)
        tracers::vars_state(m.tracers, st, FT)
    end
end

"""
    vars_state(m::AtmosModel, ::GradientFlux, FT)

Post-transform gradient variables.
"""
function vars_state(m::AtmosModel, st::GradientFlux, FT)
    @vars begin
        ∇h_tot::SVector{3, FT}
        turbulence::vars_state(m.turbulence, st, FT)
        turbconv::vars_state(m.turbconv, st, FT)
        hyperdiffusion::vars_state(m.hyperdiffusion, st, FT)
        moisture::vars_state(m.moisture, st, FT)
        tracers::vars_state(m.tracers, st, FT)
    end
end

"""
    vars_state(m::AtmosModel, ::GradientLaplacian, FT)

Pre-transform hyperdiffusive variables.
"""
function vars_state(m::AtmosModel, st::GradientLaplacian, FT)
    @vars begin
        hyperdiffusion::vars_state(m.hyperdiffusion, st, FT)
    end
end

"""
    vars_state(m::AtmosModel, ::Hyperdiffusive, FT)

Post-transform hyperdiffusive variables.
"""
function vars_state(m::AtmosModel, st::Hyperdiffusive, FT)
    @vars begin
        hyperdiffusion::vars_state(m.hyperdiffusion, st, FT)
    end
end

"""
    vars_state(m::AtmosModel, ::Auxiliary, FT)

Auxiliary variables, such as vertical (stack) integrals, coordinates,
orientation information, reference states, subcomponent auxiliary vars,
debug variables.
"""
function vars_state(m::AtmosModel, st::Auxiliary, FT)
    @vars begin
        ∫dz::vars_state(m, UpwardIntegrals(), FT)
        ∫dnz::vars_state(m, DownwardIntegrals(), FT)
        coord::SVector{3, FT}
        orientation::vars_state(m.orientation, st, FT)
        ref_state::vars_state(m.ref_state, st, FT)
        turbulence::vars_state(m.turbulence, st, FT)
        turbconv::vars_state(m.turbconv, st, FT)
        hyperdiffusion::vars_state(m.hyperdiffusion, st, FT)
        moisture::vars_state(m.moisture, st, FT)
        tracers::vars_state(m.tracers, st, FT)
        radiation::vars_state(m.radiation, st, FT)
    end
end

"""
    vars_state(m::AtmosModel, ::UpwardIntegrals, FT)
"""
function vars_state(m::AtmosModel, st::UpwardIntegrals, FT)
    @vars begin
        radiation::vars_state(m.radiation, st, FT)
        turbconv::vars_state(m.turbconv, st, FT)
    end
end

"""
    vars_state(m::AtmosModel, ::DownwardIntegrals, FT)
"""
function vars_state(m::AtmosModel, st::DownwardIntegrals, FT)
    @vars begin
        radiation::vars_state(m.radiation, st, FT)
    end
end

####
#### Forward orientation methods
####
projection_normal(bl, aux, u⃗) =
    projection_normal(bl.orientation, bl.param_set, aux, u⃗)
projection_tangential(bl, aux, u⃗) =
    projection_tangential(bl.orientation, bl.param_set, aux, u⃗)
latitude(bl, aux) = latitude(bl.orientation, aux)
longitude(bl, aux) = longitude(bl.orientation, aux)
altitude(bl, aux) = altitude(bl.orientation, bl.param_set, aux)
vertical_unit_vector(bl, aux) =
    vertical_unit_vector(bl.orientation, bl.param_set, aux)
gravitational_potential(bl, aux) = gravitational_potential(bl.orientation, aux)
∇gravitational_potential(bl, aux) =
    ∇gravitational_potential(bl.orientation, aux)

turbulence_tensors(atmos::AtmosModel, args...) =
    turbulence_tensors(atmos.turbulence, atmos, args...)

include("problem.jl")
include("ref_state.jl")
include("moisture.jl")
include("thermo_states.jl")
include("precipitation.jl")
include("radiation.jl")
include("source.jl")
include("tracers.jl")
include("linear.jl")
include("courant.jl")
include("filters.jl")

"""
    flux_first_order!(
        m::AtmosModel,
        flux::Grad,
        state::Vars,
        aux::Vars,
        t::Real
    )

Computes and assembles non-diffusive fluxes in the model
equations.
"""
@inline function flux_first_order!(
    m::AtmosModel,
    flux::Grad,
    state::Vars,
    aux::Vars,
    t::Real,
    direction,
)
    ρ = state.ρ
    ρinv = 1 / ρ
    ρu = state.ρu
    u = ρinv * ρu

    # advective terms
    flux.ρ = ρ * u
    flux.ρu = ρ * u .* u'
    flux.ρe = u * state.ρe

    # pressure terms
    ts = recover_thermo_state(m, state, aux)
    p = air_pressure(ts)
    if m.ref_state isa HydrostaticState
        flux.ρu += (p - aux.ref_state.p) * I
    else
        flux.ρu += p * I
    end
    flux.ρe += u * p
    flux_radiation!(m.radiation, m, flux, state, aux, t)
    flux_moisture!(m.moisture, m, flux, state, aux, t)
    flux_tracers!(m.tracers, m, flux, state, aux, t)
    flux_first_order!(m.turbconv, m, flux, state, aux, t)
end

function compute_gradient_argument!(
    atmos::AtmosModel,
    transform::Vars,
    state::Vars,
    aux::Vars,
    t::Real,
)
    ρinv = 1 / state.ρ
    transform.u = ρinv * state.ρu
    ts = recover_thermo_state(atmos, state, aux)
    e_tot = state.ρe * (1 / state.ρ)
    transform.h_tot = total_specific_enthalpy(ts, e_tot)

    compute_gradient_argument!(atmos.moisture, transform, state, aux, t)
    compute_gradient_argument!(atmos.turbulence, transform, state, aux, t)
    compute_gradient_argument!(
        atmos.hyperdiffusion,
        atmos,
        transform,
        state,
        aux,
        t,
    )
    compute_gradient_argument!(atmos.tracers, transform, state, aux, t)
    compute_gradient_argument!(atmos.turbconv, atmos, transform, state, aux, t)
end

function compute_gradient_flux!(
    atmos::AtmosModel,
    diffusive::Vars,
    ∇transform::Grad,
    state::Vars,
    aux::Vars,
    t::Real,
)
    diffusive.∇h_tot = ∇transform.h_tot

    # diffusion terms required for SGS turbulence computations
    compute_gradient_flux!(
        atmos.turbulence,
        atmos.orientation,
        diffusive,
        ∇transform,
        state,
        aux,
        t,
    )
    # diffusivity of moisture components
    compute_gradient_flux!(atmos.moisture, diffusive, ∇transform, state, aux, t)
    compute_gradient_flux!(atmos.tracers, diffusive, ∇transform, state, aux, t)
    compute_gradient_flux!(
        atmos.turbconv,
        atmos,
        diffusive,
        ∇transform,
        state,
        aux,
        t,
    )
end

function transform_post_gradient_laplacian!(
    atmos::AtmosModel,
    hyperdiffusive::Vars,
    hypertransform::Grad,
    state::Vars,
    aux::Vars,
    t::Real,
)
    transform_post_gradient_laplacian!(
        atmos.hyperdiffusion,
        atmos,
        hyperdiffusive,
        hypertransform,
        state,
        aux,
        t,
    )
end

"""
    flux_second_order!(
        atmos::AtmosModel,
        flux::Grad,
        state::Vars,
        diffusive::Vars,
        hyperdiffusive::Vars,
        aux::Vars,
        t::Real
    )
Diffusive fluxes in AtmosModel. Viscosity, diffusivity are calculated
in the turbulence subcomponent and accessed within the diffusive flux
function. Contributions from subcomponents are then assembled (pointwise).
"""
@inline function flux_second_order!(
    atmos::AtmosModel,
    flux::Grad,
    state::Vars,
    diffusive::Vars,
    hyperdiffusive::Vars,
    aux::Vars,
    t::Real,
)
    ν, D_t, τ = turbulence_tensors(atmos, state, diffusive, aux, t)
    ν, D_t, τ = sponge_viscosity_modifier(atmos, atmos.viscoussponge, ν, D_t, τ, aux)
    d_h_tot = -D_t .* diffusive.∇h_tot
    flux_second_order!(atmos, flux, state, τ, d_h_tot)
    flux_second_order!(atmos.moisture, flux, state, diffusive, aux, t, D_t)
    flux_second_order!(
        atmos.hyperdiffusion,
        flux,
        state,
        diffusive,
        hyperdiffusive,
        aux,
        t,
    )
    flux_second_order!(atmos.tracers, flux, state, diffusive, aux, t, D_t)
    flux_second_order!(atmos.turbconv, atmos, flux, state, diffusive, aux, t)
end

#TODO: Consider whether to not pass ρ and ρu (not state), foc BCs reasons
@inline function flux_second_order!(
    atmos::AtmosModel,
    flux::Grad,
    state::Vars,
    τ,
    d_h_tot,
)
    flux.ρu += τ * state.ρ
    flux.ρe += τ * state.ρu
    flux.ρe += d_h_tot * state.ρ
end

@inline function wavespeed(
    m::AtmosModel,
    nM,
    state::Vars,
    aux::Vars,
    t::Real,
    direction,
)
    ρinv = 1 / state.ρ
    u = ρinv * state.ρu
    uN = abs(dot(nM, u))
    ts = recover_thermo_state(m, state, aux)
    ss = soundspeed_air(ts)

    FT = typeof(state.ρ)
    ws = fill(uN + ss, MVector{number_states(m, Prognostic()), FT})
    vars_ws = Vars{vars_state(m, Prognostic(), FT)}(ws)

    wavespeed_tracers!(m.tracers, vars_ws, nM, state, aux, t)

    return ws
end

function update_auxiliary_state!(
    dg::DGModel,
    m::AtmosModel,
    Q::MPIStateArray,
    t::Real,
    elems::UnitRange,
)
    FT = eltype(Q)
    state_auxiliary = dg.state_auxiliary

    if number_states(m, UpwardIntegrals()) > 0
        indefinite_stack_integral!(dg, m, Q, state_auxiliary, t, elems)
        reverse_indefinite_stack_integral!(dg, m, Q, state_auxiliary, t, elems)
    end

    update_auxiliary_state!(nodal_update_auxiliary_state!, dg, m, Q, t, elems)

    # TODO: Remove this hook. This hook was added for implementing
    # the first draft of EDMF, and should be removed so that we can
    # rely on a single vertical element traversal. This hook allows
    # us to compute globally vertical quantities specific to EDMF
    # until we're able to remove them or somehow incorporate them
    # into a higher level hierarchy.
    update_auxiliary_state!(dg, m.turbconv, m, Q, t, elems)

    return true
end

function nodal_update_auxiliary_state!(
    m::AtmosModel,
    state::Vars,
    aux::Vars,
    t::Real,
)
    atmos_nodal_update_auxiliary_state!(m.moisture, m, state, aux, t)
    atmos_nodal_update_auxiliary_state!(m.radiation, m, state, aux, t)
    atmos_nodal_update_auxiliary_state!(m.tracers, m, state, aux, t)
    turbulence_nodal_update_auxiliary_state!(m.turbulence, m, state, aux, t)
    turbconv_nodal_update_auxiliary_state!(m.turbconv, m, state, aux, t)
end

function integral_load_auxiliary_state!(
    m::AtmosModel,
    integ::Vars,
    state::Vars,
    aux::Vars,
)
    integral_load_auxiliary_state!(m.radiation, integ, state, aux)
    integral_load_auxiliary_state!(m.turbconv, m, integ, state, aux)
end

function integral_set_auxiliary_state!(m::AtmosModel, aux::Vars, integ::Vars)
    integral_set_auxiliary_state!(m.radiation, aux, integ)
    integral_set_auxiliary_state!(m.turbconv, m, aux, integ)
end

function reverse_integral_load_auxiliary_state!(
    m::AtmosModel,
    integ::Vars,
    state::Vars,
    aux::Vars,
)
    reverse_integral_load_auxiliary_state!(m.radiation, integ, state, aux)
end

function reverse_integral_set_auxiliary_state!(
    m::AtmosModel,
    aux::Vars,
    integ::Vars,
)
    reverse_integral_set_auxiliary_state!(m.radiation, aux, integ)
end

function atmos_nodal_init_state_auxiliary!(
    m::AtmosModel,
    aux::Vars,
    tmp::Vars,
    geom::LocalGeometry,
)
    aux.coord = geom.coord
    init_aux_turbulence!(m.turbulence, m, aux, geom)
    atmos_init_aux!(m.ref_state, m, aux, tmp, geom)
    init_aux_hyperdiffusion!(m.hyperdiffusion, m, aux, geom)
    atmos_init_aux!(m.tracers, m, aux, geom)
    init_aux_turbconv!(m.turbconv, m, aux, geom)
    m.problem.init_state_auxiliary(m.problem, m, aux, geom)
end

"""
    init_state_auxiliary!(
        m::AtmosModel,
        aux::Vars,
        grid,
        direction
    )

Initialise auxiliary variables for each AtmosModel subcomponent.
Store Cartesian coordinate information in `aux.coord`.
"""
function init_state_auxiliary!(
    m::AtmosModel,
    state_auxiliary::MPIStateArray,
    grid,
    direction,
)
    init_aux!(m, m.orientation, state_auxiliary, grid, direction)

    init_state_auxiliary!(
        m,
        (m, aux, tmp, geom) ->
            atmos_init_ref_state_pressure!(m.ref_state, m, aux, geom),
        state_auxiliary,
        grid,
        direction,
    )

    ∇p = ∇reference_pressure(m.ref_state, state_auxiliary, grid)

    init_state_auxiliary!(
        m,
        atmos_nodal_init_state_auxiliary!,
        state_auxiliary,
        grid,
        direction;
        state_temporary = ∇p,
    )
end

"""
    source!(
        m::AtmosModel,
        source::Vars,
        state::Vars,
        diffusive::Vars,
        aux::Vars,
        t::Real,
        direction::Direction,
    )

Computes (and assembles) source terms `S(Y)` in:
```
∂Y
-- = - ∇ • F + S(Y)
∂t
```
"""
function source!(
    m::AtmosModel,
    source::Vars,
    state::Vars,
    diffusive::Vars,
    aux::Vars,
    t::Real,
    direction,
)
    atmos_source!(m.source, m, source, state, diffusive, aux, t, direction)
end

"""
    init_state_prognostic!(
        m::AtmosModel,
        state::Vars,
        aux::Vars,
        coords,
        t,
        args...,
    )

Initialise state variables. `args...` provides an option to include
configuration data (current use cases include problem constants,
spline-interpolants).
"""
function init_state_prognostic!(
    m::AtmosModel,
    state::Vars,
    aux::Vars,
    coords,
    t,
    args...,
)
    m.problem.init_state_prognostic(
        m.problem,
        m,
        state,
        aux,
        coords,
        t,
        args...,
    )
end

roe_average(ρ⁻, ρ⁺, var⁻, var⁺) =
    (sqrt(ρ⁻) * var⁻ + sqrt(ρ⁺) * var⁺) / (sqrt(ρ⁻) + sqrt(ρ⁺))

function numerical_flux_first_order!(
    numerical_flux::RoeNumericalFlux,
    balance_law::AtmosModel,
    fluxᵀn::Vars{S},
    normal_vector::SVector,
    state_prognostic⁻::Vars{S},
    state_auxiliary⁻::Vars{A},
    state_prognostic⁺::Vars{S},
    state_auxiliary⁺::Vars{A},
    t,
    direction,
) where {S, A}
    @assert balance_law.moisture isa DryModel

    numerical_flux_first_order!(
        CentralNumericalFluxFirstOrder(),
        balance_law,
        fluxᵀn,
        normal_vector,
        state_prognostic⁻,
        state_auxiliary⁻,
        state_prognostic⁺,
        state_auxiliary⁺,
        t,
        direction,
    )

    FT = eltype(fluxᵀn)
    param_set = balance_law.param_set
    _cv_d::FT = cv_d(param_set)
    _T_0::FT = T_0(param_set)

    Φ = gravitational_potential(balance_law, state_auxiliary⁻)

    ρ⁻ = state_prognostic⁻.ρ
    ρu⁻ = state_prognostic⁻.ρu
    ρe⁻ = state_prognostic⁻.ρe
    ts⁻ = recover_thermo_state(
        balance_law,
        balance_law.moisture,
        state_prognostic⁻,
        state_auxiliary⁻,
    )

    u⁻ = ρu⁻ / ρ⁻
    uᵀn⁻ = u⁻' * normal_vector
    e⁻ = ρe⁻ / ρ⁻
    h⁻ = total_specific_enthalpy(ts⁻, e⁻)
    p⁻ = air_pressure(ts⁻)
    c⁻ = soundspeed_air(ts⁻)

    ρ⁺ = state_prognostic⁺.ρ
    ρu⁺ = state_prognostic⁺.ρu
    ρe⁺ = state_prognostic⁺.ρe

    # TODO: state_auxiliary⁺ is not up-to-date
    # with state_prognostic⁺ on the boundaries
    ts⁺ = recover_thermo_state(
        balance_law,
        balance_law.moisture,
        state_prognostic⁺,
        state_auxiliary⁺,
    )

    u⁺ = ρu⁺ / ρ⁺
    uᵀn⁺ = u⁺' * normal_vector
    e⁺ = ρe⁺ / ρ⁺
    h⁺ = total_specific_enthalpy(ts⁺, e⁺)
    p⁺ = air_pressure(ts⁺)
    c⁺ = soundspeed_air(ts⁺)

    ρ̃ = sqrt(ρ⁻ * ρ⁺)
    ũ = roe_average(ρ⁻, ρ⁺, u⁻, u⁺)
    h̃ = roe_average(ρ⁻, ρ⁺, h⁻, h⁺)
    c̃ = sqrt(roe_average(ρ⁻, ρ⁺, c⁻^2, c⁺^2))

    ũᵀn = ũ' * normal_vector

    Δρ = ρ⁺ - ρ⁻
    Δp = p⁺ - p⁻
    Δu = u⁺ - u⁻
    Δuᵀn = Δu' * normal_vector

    w1 = abs(ũᵀn - c̃) * (Δp - ρ̃ * c̃ * Δuᵀn) / (2 * c̃^2)
    w2 = abs(ũᵀn + c̃) * (Δp + ρ̃ * c̃ * Δuᵀn) / (2 * c̃^2)
    w3 = abs(ũᵀn) * (Δρ - Δp / c̃^2)
    w4 = abs(ũᵀn) * ρ̃

    fluxᵀn.ρ -= (w1 + w2 + w3) / 2
    fluxᵀn.ρu -=
        (
            w1 * (ũ - c̃ * normal_vector) +
            w2 * (ũ + c̃ * normal_vector) +
            w3 * ũ +
            w4 * (Δu - Δuᵀn * normal_vector)
        ) / 2
    fluxᵀn.ρe -=
        (
            w1 * (h̃ - c̃ * ũᵀn) +
            w2 * (h̃ + c̃ * ũᵀn) +
            w3 * (ũ' * ũ / 2 + Φ - _T_0 * _cv_d) +
            w4 * (ũ' * Δu - ũᵀn * Δuᵀn)
        ) / 2

    if !(balance_law.tracers isa NoTracers)
        ρχ⁻ = state_prognostic⁻.tracers.ρχ
        χ⁻ = ρχ⁻ / ρ⁻

        ρχ⁺ = state_prognostic⁺.tracers.ρχ
        χ⁺ = ρχ⁺ / ρ⁺

        χ̃ = roe_average(ρ⁻, ρ⁺, χ⁻, χ⁺)
        Δρχ = ρχ⁺ - ρχ⁻

        wt = abs(ũᵀn) * (Δρχ - χ̃ * Δp / c̃^2)

        fluxᵀn.tracers.ρχ -= ((w1 + w2) * χ̃ + wt) / 2
    end
end

"""
    NumericalFluxFirstOrder()
        ::HLLCNumericalFlux,
        balance_law::AtmosModel,
        fluxᵀn,
        normal_vector,
        state_prognostic⁻,
        state_auxiliary⁻,
        state_prognostic⁺,
        state_auxiliary⁺,
        t,
        direction,
    )

An implementation of the numerical flux based on the HLLC method for
the AtmosModel. For more information on this particular implementation,
see Chapter 10.4 in the provided reference below.

## References
    @book{toro2013riemann,
        title={Riemann solvers and numerical methods for fluid dynamics: a practical introduction},
        author={Toro, Eleuterio F},
        year={2013},
        publisher={Springer Science & Business Media}
    }
"""
function numerical_flux_first_order!(
    ::HLLCNumericalFlux,
    balance_law::AtmosModel,
    fluxᵀn::Vars{S},
    normal_vector::SVector,
    state_prognostic⁻::Vars{S},
    state_auxiliary⁻::Vars{A},
    state_prognostic⁺::Vars{S},
    state_auxiliary⁺::Vars{A},
    t,
    direction,
) where {S, A}
    FT = eltype(fluxᵀn)
    num_state_prognostic = number_states(balance_law, Prognostic())
    param_set = balance_law.param_set

    # Extract the first-order fluxes from the AtmosModel (underlying BalanceLaw)
    # and compute normals on the positive + and negative - sides of the
    # interior facets
    flux⁻ = similar(parent(fluxᵀn), Size(3, num_state_prognostic))
    fill!(flux⁻, -zero(FT))
    flux_first_order!(
        balance_law,
        Grad{S}(flux⁻),
        state_prognostic⁻,
        state_auxiliary⁻,
        t,
        direction,
    )
    fluxᵀn⁻ = flux⁻' * normal_vector

    flux⁺ = similar(flux⁻)
    fill!(flux⁺, -zero(FT))
    flux_first_order!(
        balance_law,
        Grad{S}(flux⁺),
        state_prognostic⁺,
        state_auxiliary⁺,
        t,
        direction,
    )
    fluxᵀn⁺ = flux⁺' * normal_vector

    # Extract relevant fields and thermodynamic variables defined on
    # the positive + and negative - sides of the interior facets
    ρ⁻ = state_prognostic⁻.ρ
    ρu⁻ = state_prognostic⁻.ρu
    ρe⁻ = state_prognostic⁻.ρe
    ts⁻ = recover_thermo_state(
        balance_law,
        balance_law.moisture,
        state_prognostic⁻,
        state_auxiliary⁻,
    )

    u⁻ = ρu⁻ / ρ⁻
    c⁻ = soundspeed_air(ts⁻)

    uᵀn⁻ = u⁻' * normal_vector
    e⁻ = ρe⁻ / ρ⁻
    p⁻ = air_pressure(ts⁻)

    ρ⁺ = state_prognostic⁺.ρ
    ρu⁺ = state_prognostic⁺.ρu
    ρe⁺ = state_prognostic⁺.ρe
    ts⁺ = recover_thermo_state(
        balance_law,
        balance_law.moisture,
        state_prognostic⁺,
        state_auxiliary⁺,
    )

    u⁺ = ρu⁺ / ρ⁺
    uᵀn⁺ = u⁺' * normal_vector
    e⁺ = ρe⁺ / ρ⁺
    p⁺ = air_pressure(ts⁺)
    c⁺ = soundspeed_air(ts⁺)

    # Wave speeds estimates S⁻ and S⁺
    S⁻ = min(uᵀn⁻ - c⁻, uᵀn⁺ - c⁺)
    S⁺ = max(uᵀn⁻ + c⁻, uᵀn⁺ + c⁺)

    # Compute the middle wave speed S⁰ in the contact/star region
    S⁰ =
        (p⁺ - p⁻ + ρ⁻ * uᵀn⁻ * (S⁻ - uᵀn⁻) - ρ⁺ * uᵀn⁺ * (S⁺ - uᵀn⁺)) /
        (ρ⁻ * (S⁻ - uᵀn⁻) - ρ⁺ * (S⁺ - uᵀn⁺))

    p⁰ =
        (
            p⁺ +
            p⁻ +
            ρ⁻ * (S⁻ - uᵀn⁻) * (S⁰ - uᵀn⁻) +
            ρ⁺ * (S⁺ - uᵀn⁺) * (S⁰ - uᵀn⁺)
        ) / 2

    # Compute p * D = p * (0, n₁, n₂, n₃, S⁰)
    pD = @MVector zeros(FT, num_state_prognostic)
    if balance_law.ref_state isa HydrostaticState
        # pressure should be continuous but it doesn't hurt to average
        ref_p⁻ = state_auxiliary⁻.ref_state.p
        ref_p⁺ = state_auxiliary⁺.ref_state.p
        ref_p⁰ = (ref_p⁻ + ref_p⁺) / 2

        momentum_p = p⁰ - ref_p⁰
    else
        momentum_p = p⁰
    end

    pD[2] = momentum_p * normal_vector[1]
    pD[3] = momentum_p * normal_vector[2]
    pD[4] = momentum_p * normal_vector[3]
    pD[5] = p⁰ * S⁰

    # Computes both +/- sides of intermediate flux term flux⁰
    flux⁰⁻ =
        (S⁰ * (S⁻ * parent(state_prognostic⁻) - fluxᵀn⁻) + S⁻ * pD) / (S⁻ - S⁰)
    flux⁰⁺ =
        (S⁰ * (S⁺ * parent(state_prognostic⁺) - fluxᵀn⁺) + S⁺ * pD) / (S⁺ - S⁰)

    if 0 <= S⁻
        parent(fluxᵀn) .= fluxᵀn⁻
    elseif S⁻ < 0 <= S⁰
        parent(fluxᵀn) .= flux⁰⁻
    elseif S⁰ < 0 <= S⁺
        parent(fluxᵀn) .= flux⁰⁺
    else # 0 > S⁺
        parent(fluxᵀn) .= fluxᵀn⁺
    end
end

end # module


# Copyright 2014 Mitchell Kember. Subject to the MIT License.
# Google Code Jam Round 1A 2014
# Problem B: ???

function mindel(r, t)
	d = 0
	stack = Int[]
	push!(stack, nothing)
	top = r
	while top !== nothing
		if top
	end
end

function main()
	n = int(readline())
	for i = 1:n
		nn = int(readline())
		tree = Array((Int, Int), nn-1)
		for j = 1:nn-1
			con = int(split(readline()))
			tree[j] = minmax(con[1], con[2])
		end
		println("Case #$i: ", mindel(tree))
	end
end

main()


# gpu

import .CUDA
import .CUDA: CuArray, CuArrayStyle, @allowscalar
import Adapt: WrappedArray

const CuOneHotArray{K, N, var"N+1"} = OneHotArray{K, N, var"N+1", <: CuArray{OneHot{K}, N}}
const WrappedCuOneHotArray{K, N} = WrappedArray{Bool, N, CuOneHotArray{K, N}, CuOneHotArray{K, N}}

Base.print_array(io::IO, oa::Union{CuOneHotArray, WrappedCuOneHotArray}) = Base.print_array(io, adapt(Array, oa))

Base._show_nonempty(io::IO, oa::Union{CuOneHotArray, WrappedCuOneHotArray}, prefix::String) =
    Base._show_nonempty(io, adapt(Array, oa), prefix)

Base.convert(::Type{T}, oa::Union{CuOneHotArray, WrappedCuOneHotArray}) where {T<:Array} =
    Base.convert(T, adapt(Array, oa))

Base.Array{T, N}(oa::Union{CuOneHotArray, WrappedCuOneHotArray}) where {T, N} = Array{T, N}(adapt(Array, oa))
Base.collect(oa::Union{CuOneHotArray, WrappedCuOneHotArray}) = collect(adapt(Array, oa))

Base.BroadcastStyle(::Type{<:CuOneHotArray{K, N}}) where {K, N} = CuArrayStyle{N+1}()

# avoid array copy
function Base.copyto!(dst::CuArray, oa::OneHotArray)
    dst .= oa
    return dst
end

CUDA.CuArray{T, N, B}(oa::Union{CuOneHotArray, WrappedCuOneHotArray}) where {T, K, N, B, var"N+1", A <: CuArray} =
    copyto!(similar(oa, T), oa)

using Base.Cartesian

function Base.findmax(oa::CuOneHotArray; dims=:)
    if dims == Colon()
        i = findfirst(!iszero, parent(oa))
        isnothing(i) && return (false, CartesianIndex{ndims(oa)}())
        v = @allowscalar oa.onehots[i]
        return (true, CartesianIndex(Int(v), i))
    elseif isone(dims)
        onehots = parent(oa)
        vs = map(!iszero, onehots)
        function f1(x, cidx)
            iszero(x) ? CartesianIndex(1, cidx) : CartesianIndex(Int(x), cidx)
        end
        is = map(f1, onehots, CartesianIndices(onehots))
        return (reshape(vs, 1, size(vs)...), reshape(is, 1, size(is)...))
    else
        return findminmax(>, oa; init=false, dims=dims)
    end
end

function findminmax(binop, a::CuOneHotArray; init, dims)
    function f(t1, t2)
        (x, i), (y, j) = t1, t2

        iszero(i) && return t2
        iszero(j) && return t1
        binop(x, y) && return t1
        x == y && return (x, Base.min(i, j))
        return t2
    end

    indx = ndims(a) == 1 ? (eachindex(a), 1) :
                           (CartesianIndices(a), zero(CartesianIndex{ndims(a)}))

    if dims == Colon()
        mapreduce(tuple, f, a, indx[1]; init = (init, indx[2]))
    else
        res = CUDA.GPUArrays._mapreduce(tuple, f, a, indx[1];
                                        init = (init, indx[2]), dims=dims)
        vals = map(x->x[1], res)
        inds = map(x->x[2], res)
        CUDA.unsafe_free!(res)
        return (vals, inds)
    end
end


"""
    train!(model::AbstractGPModel; iterations::Integer=100, callback, convergence)

Function to train the given GP `model`.
## Arguments
- `model` : AbstractGPModel model with either an `Analytic`, `AnalyticVI` or `NumericalVI` type of inference

## Keyword Arguments
- `iterations::Int` : Number of iterations (not necessarily epochs!)for training
- `callback::Function=nothing` : Callback function called at every iteration. Should be of type `function(model,iter) ...  end`
- `convergence::Function=nothing` : Convergence function to be called every iteration, should return a scalar and take the same arguments as `callback`
"""
function train!(
    model::AbstractGPModel{T},
    X::AbstractArray,
    y,
    iterations::Int=100;
    callback=nothing,
    convergence=nothing,
    state=nothing,
    obsdim=1,
) where {T}
    iterations > 0 || error("Number of iterations should be positive")
    X, Tx = wrap_X(X, obsdim)
    data = wrap_data(X, y, likelihood(model))
    if is_stochastic(model)
        0 < batchsize(inference(model)) <= n_sample(data) || error(
            "The size of mini-batch $(batchsize(inference(model))) is incorrect (negative or bigger than number of samples), please set `batchsize` correctly in the inference object",
        )
        set_ρ!(model, n_sample(data) / batchsize(inference(model)))
    else
        set_batchsize!(inference(model), n_sample(data))
    end

    if verbose(model) > 0
        @info "Starting training $model with $(n_sample(data)) samples, $(n_dim(data)) features and $(n_latent(likelihood(model))) latent GP" *
            (n_latent(model) > 1 ? "s" : "")
    end
    # model.evol_conv = [] # Array to check on the evolution of convergence
    local_iter = 1
    if isnothing(state)
        setHPupdated!(inference(model), true)
    end
    state = isnothing(state) ? init_state(model) : state
    conv = Inf
    p = Progress(iterations; dt=0.2, desc="Training Progress: ")
    prev_elbo = -Inf
    while true # loop until one condition is matched
        try
            if is_stochastic(model)
                minibatch = StatsBase.sample(
                    1:n_sample(data), batchsize(model); replace=false
                )
                x = view_x(data, minibatch)
                y = view_y(likelihood(model), data, minibatch)
            else
                x = input(data)
                y = view_y(likelihood(model), data, 1:n_sample(data))
            end
            state = update_parameters!(model, state, x, y) #Update all the variational parameters
            set_trained!(model, true)
            if !isnothing(callback)
                callback(model, state, n_iter(model)) #Use a callback method if set by user
            end
            if (n_iter(model) % model.atfrequency == 0) &&
                (n_iter(model) >= 3) &&
                (local_iter != iterations)
                state = update_hyperparameters!(model, state, x, y) #Update the hyperparameters
            end
            # Print out informations about the convergence
            if verbose(model) > 2 || (verbose(model) > 1 && local_iter % 10 == 0)
                if inference(model) isa GibbsSampling
                    next!(p; showvalues=[(:samples, local_iter)])
                else
                    if (verbose(model) == 2 && local_iter % 10 == 0)
                        elbo = objective(model, state, y)
                        prev_elbo = elbo
                        ProgressMeter.update!(p, local_iter - 1)
                        ProgressMeter.next!(
                            p; showvalues=[(:iter, local_iter), (:ELBO, elbo)]
                        )
                    elseif verbose(model) > 2
                        elbo = objective(model, state, y)
                        prev_elbo = elbo
                        ProgressMeter.next!(
                            p; showvalues=[(:iter, local_iter), (:ELBO, prev_elbo)]
                        )
                    end
                end
            end
            local_iter += 1
            model.inference.n_iter += 1
            (local_iter <= iterations) || break #Verify if the number of maximum iterations has been reached
        # (iter < model.nEpochs && conv > model.ϵ) || break; #Verify if any condition has been broken
        catch e
            if isa(e, InterruptException)
                @warn "Training interrupted by user at iteration $local_iter"
                break
            else
                rethrow(e)
            end
        end
    end
    if verbose(model) > 0
        @info "Training ended after $(local_iter - 1) iterations. Total number of iterations $(n_iter(model))"
    end
    state = merge(state, compute_Ks(model)) # Compute final version of the matrices for predictions
    post_step!(model, state)
    set_trained!(model, true)
    return model, state
end

@traitfn function train!(
    model::TGP, iterations; callback=nothing, convergence=nothing, state=nothing
) where {TGP <: AbstractGPModel; IsFull{TGP}}
    return train!(
        model,
        input(model.data),
        output(model.data),
        iterations;
        state,
        callback,
        convergence,
    )
end

function update_parameters!(model::GP, state, x, y)
    state = compute_kernel_matrices(model, state, x) # Recompute the matrices if necessary (when hyperparameters have been updated)
    state = analytic_updates(model, state, y)
    return state
end

function update_parameters!(model::VGP, state, x, y)
    state = compute_kernel_matrices(model, state, x) # Recompute the matrices if necessary (always for the stochastic case, or when hyperparameters have been updated)
    state = variational_updates(model, state, y)
    return state
end

## Update all variational parameters of the sparse variational GP Model ##
function update_parameters!(model::SVGP, state, x, y)
    state = compute_kernel_matrices(model, state, x) # Recompute the matrices if necessary (always for the stochastic case, or when hyperparameters have been updated)
    state = variational_updates(model, state, y)
    return state
end

function update_parameters!(model::MOVGP, state, x, y)
    state = compute_kernel_matrices(model, state, x) # Recompute the matrices if necessary (always for the stochastic case, or when hyperparameters have been updated)
    state = update_A!(model, state, y)
    state = variational_updates(model, state, y)
    return state
end

function update_parameters!(m::MOSVGP, state, x, y)
    state = compute_kernel_matrices(m, state, x) # Recompute the matrices if necessary (always for the stochastic case, or when hyperparameters have been updated)
    update_A!(m, state, y)
    state = variational_updates(m, state, y)
    return state
end

function update_parameters!(m::VStP, state, x, y)
    state = compute_kernel_matrices(m, state, x) # Recompute the matrices if necessary (always for the stochastic case, or when hyperparameters have been updated)
    state = local_prior_updates!(m, state, x)
    state = variational_updates(m, state, y)
    return state
end

function compute_kernel_matrices(m::GP{T}, state, x, ::Bool) where {T}
    kernel_matrices = (; K=compute_K(m.f, x, T(jitt)))
    setHPupdated!(inference(m), false)
    return merge(state, (; kernel_matrices))
end

@traitfn function compute_kernel_matrices(
    m::TGP, state, x, update::Bool=false
) where {T,TGP<:AbstractGPModel{T};IsFull{TGP}}
    if isHPupdated(inference(m)) || update
        kernel_matrices = map(m.f) do gp
            (; K=compute_K(gp, x, T(jitt)))
        end
        setHPupdated!(inference(m), false)
        return merge(state, (; kernel_matrices))
    else
        return state
    end
end

@traitfn function compute_kernel_matrices(
    m::TGP, state, x, update::Bool=false
) where {T,TGP<:AbstractGPModel{T};!IsFull{TGP}}
    kernel_matrices = if isHPupdated(inference(m)) || update
        map(m.f) do gp
            (; K=compute_K(gp, T(jitt)))
        end
    else
        state.kernel_matrices
    end
    # If change of hyperparameters or if stochatic
    kernel_matrices = if isHPupdated(inference(m)) || is_stochastic(inference(m)) || update
        κs = map(m.f, kernel_matrices) do gp, kernel_matrix
            compute_κ(gp, x, kernel_matrix.K, T(jitt))
        end
        merge.(kernel_matrices, κs)
    else
        kernel_matrices
    end
    setHPupdated!(inference(m), false)
    return merge(state, (; kernel_matrices)) # update the kernel_matrices state
end

function compute_Ks(m::AbstractGPModel{T}) where {T}
    return (; kernel_matrices=broadcast(m.f, Zviews(m)) do gp, x
        K = compute_K(gp, x, T(jitt))
        return (; K)
    end)
end

function compute_Ks(m::GP{T}) where {T}
    return (; kernel_matrices=(; K=compute_K(m.f, input(m.data), T(jitt))))
end


using BioSequences

#AG
println("running program")

seq = randdnaseq(50) # random seed appears fixed
println(seq)


module JSONWebTokens

#=
Algorithms

HS256   HMAC using SHA-256 hash algorithm
HS384   HMAC using SHA-384 hash algorithm
HS512   HMAC using SHA-512 hash algorithm
RS256   RSASSA using SHA-256 hash algorithm
RS384   RSASSA using SHA-384 hash algorithm
RS512   RSASSA using SHA-512 hash algorithm
ES256   ECDSA using P-256 curve and SHA-256 hash algorithm
ES384   ECDSA using P-384 curve and SHA-384 hash algorithm
ES512   ECDSA using P-521 curve and SHA-512 hash algorithm
none    No digital signature or MAC value included
=#

import JSON, SHA, MbedTLS
import Base64
using Random

abstract type Encoding end
function alg end
Base.show(io::IO, encoding::Encoding) = print(io, alg(encoding))

include("errors.jl")

include("base64url/Base64URL.jl")

include("jws.jl")
include("none.jl")
include("hs.jl")
include("rs.jl")

end


#  MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
#  Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
#  Released under the modified BSD license. See COPYING.md for more details.

using Cbc
using CSV
using DataFrames

@testset "BenchmarkRunner" begin
    @info "Building training data..."
    instances = [build_knapsack_file_instance(), build_knapsack_file_instance()]
    stats = parallel_solve!(LearningSolver(Cbc.Optimizer), instances)
    @test length(stats) == 2
    @test stats[1] !== nothing
    @test stats[2] !== nothing

    benchmark = BenchmarkRunner(
        solvers = Dict(
            "baseline" => LearningSolver(Cbc.Optimizer, components = []),
            "ml-exact" => LearningSolver(Cbc.Optimizer),
            "ml-heur" => LearningSolver(Cbc.Optimizer, mode = "heuristic"),
        ),
    )
    @info "Fitting..."
    fit!(benchmark, instances)

    @info "Benchmarking..."
    parallel_solve!(benchmark, instances, n_trials = 2)

    csv_filename = tempname()
    write_csv!(benchmark, csv_filename)
    @test isfile(csv_filename)
    csv = DataFrame(CSV.File(csv_filename))
    @test size(csv)[1] == 12
end


"""
The following file contains simple abstractions to make working with
posix permissions easier in julia.

For now we're focused on user permissions.

A lot of the low level permissions code below and the corresponding
constants have been translated from cpython's Lib/stat.py file.

https://github.com/python/cpython/blob/master/Lib/stat.py
"""
immutable Mode
    m::UInt64
end

function Mode(;user::UInt8=0o0, group::UInt8=0o0, other::UInt8=0o0,)
    @assert user <= 0o7 && group <= 0o7 && other <= 0o7

    Mode(user * USER_COEFF | group * GROUP_COEFF | other * OTHER_COEFF)
end

function Mode(mode::UInt8, usr_grps::Symbol...)
    user = group = other = 0o0

    for usr_grp in usr_grps
        if usr_grp == :ALL
            user = group = other = mode
            break
        elseif usr_grp == :USER
            user = mode
        elseif usr_grp == :GROUP
            group = mode
        elseif usr_grp == :OTHER
            other = mode
        else
            error(
                "$usr_grp not a valid symbol only \
                :ALL, :USER, :GROUP and :OTHER are accepted."
            )
        end
    end

    return Mode(user=user, group=group, other=other)
end

Base.show(io::IO, mode::Mode) = print(io, string(mode))

Base.(:-)(a::Mode, b::Mode) = Mode(a.m & ~b.m)
Base.(:+)(a::Mode, b::Mode) = Mode(a.m | b.m)

executable(usr_grps::Symbol...) = Mode(EXEC, usr_grps...)
readable(usr_grps::Symbol...) = Mode(READ, usr_grps...)
writable(usr_grps::Symbol...) = Mode(WRITE, usr_grps...)

function Base.isexecutable(mode::Mode, usr_grp::Symbol)
    mask = S_IXOTH | S_IXGRP | S_IXUSR

    if usr_grp == :OTHER
        mask = S_IXOTH
    elseif usr_grp == :GROUP
        mask = S_IXGRP
    elseif usr_grp == :USER
        mask = S_IXUSR
    end

    return mode.m & mask == mask
end

function Base.iswritable(mode::Mode, usr_grp::Symbol)
    mask = S_IWOTH | S_IWGRP | S_IWUSR

    if usr_grp == :OTHER
        mask = S_IWOTH
    elseif usr_grp == :GROUP
        mask = S_IWGRP
    elseif usr_grp == :USER
        mask = S_IWUSR
    end

    return mode.m & mask == mask
end

function Base.isreadable(mode::Mode, usr_grp::Symbol)
    mask = S_IROTH | S_IRGRP | S_IRUSR

    if usr_grp == :OTHER
        mask = S_IROTH
    elseif usr_grp == :GROUP
        mask = S_IRGRP
    elseif usr_grp == :USER
        mask = S_IRUSR
    end

    return mode.m & mask == mask
end

Base.oct(mode::Mode) = oct(mode.m)
raw(mode::Mode) = mode.m

"""Return True if mode is from a directory."""
Base.isdir(mode::Mode) = _meta(mode.m) == S_IFDIR

"""Return True if mode is from a regular file."""
Base.isfile(mode::Mode) = _meta(mode.m) == S_IFREG

"""Return True if mode is from a symbolic link."""
Base.islink(mode::Mode) = _meta(mode.m) == S_IFLNK

"""Return True if mode is from a socket."""
Base.issocket(mode) = _meta(mode.m) == S_IFSOCK

"""Return True if mode is from a FIFO (named pipe)."""
Base.isfifo(mode::Mode) = _meta(mode.m) == S_IFIFO

"""Return True if mode is from a character special device file."""
Base.ischardev(mode::Mode) = _meta(mode.m) == S_IFCHR

"""Return True if mode is from a block special device file."""
Base.isblockdev(mode::Mode) = _meta(mode.m) == S_IFBLK

"""Convert a file's mode to a string of the form '-rwxrwxrwx'."""
function Base.string(mode::Mode)
    perm = []
    for table in FILEMODE_TABLE
        found = false
        for (bit, char) in table
            if mode.m & bit == bit
                push!(perm, char)
                found = true
                break
            end
        end
        if !found
            push!(perm, '-')
        end
    end

    return join(perm)
end

"""
Return the portion of the file's mode that can be set by
os.chmod().
"""
_mode(mode) = mode & 0o7777

"""
Return the portion of the file's mode that describes the
file type.
"""
_meta(mode) = mode & 0o170000


_has_aarch64_sve() = (Base.libllvm_version ≥ v"11") && (Base.BinaryPlatforms.CPUID.test_cpu_feature(Base.BinaryPlatforms.CPUID.JL_AArch64_sve))

if Int === Int64
    @noinline vscale() = ccall("llvm.vscale.i64", llvmcall, Int64, ())
else
    @noinline vscale() = ccall("llvm.vscale.i32", llvmcall, Int32, ())
end

# TODO: find actually support SVE
# _dynamic_register_size() = _has_aarch64_sve() ? 16vscale() : 16
function _dynamic_register_size()
  _has_aarch64_sve() || return 16
  llvmargs = get(ENV, "JULIA_LLVM_ARGS", nothing)
  llvmargs === nothing && return 16
  m = match(r"--aarch64-sve-vector-bits-min=(\d+)", llvmargs)
  m === nothing && return 16
  length(m.captures) == 0 && return 16
  return max(16, parse(Int, last(m.captures)) ÷ 8)
end

function _set_sve_vector_width!(bytes = _dynamic_register_size())
    @eval begin
        register_size() = StaticInt{$bytes}()
        simd_integer_register_size() = StaticInt{$bytes}()
    end
    nothing
end

if _has_aarch64_sve()# && !(Bool(has_feature(Val(:aarch64_sve))))
    has_feature(::Val{:aarch64_sve_cpuid}) = True()
    _set_sve_vector_width!()
else
    # has_feature(::Val{:aarch64_svejl}) = False()
    register_size() = StaticInt{16}()
    simd_integer_register_size() = StaticInt{16}()
end

function reset_extra_features!()
    drs = _dynamic_register_size()
    register_size() ≠ drs && _set_sve_vector_width!(drs)
    hassve = _has_aarch64_sve()
    if hassve ≠ has_feature(Val(:aarch64_sve_cpuid))
        @eval has_feature(::Val{:aarch64_sve_cpuid}) = $(Expr(:call, hassve ? :True : :False))
    end
end

fma_fast() = True()
register_count() = StaticInt{32}()
has_opmask_registers() = has_feature(Val(:aarch64_sve_cpuid))

fast_int64_to_double() = True()

fast_half() = False()


function ConstructionHeuristic(order::Array{NamedTuple{(:j, :prob),Tuple{Int, Float64}}, 1}, crit::String, tasks_by_w::Dict{Int, LTask}, prec::Dict{Int, Array}, bj::Array{Int, 1}, LS::LoadingSequence, TIME::Timer, QC::Array{QuayCrane, 1}, CTS::Constants)
    for tuple in order
        target_bay = tuple.j
        #select a task within the target_bay
        for p in bay_to_pos(target_bay, bj)
            if check_prec(LS, tasks_by_w[p], prec) == true
                if check_loaded_filled(LS, tasks_by_w[p]) == true
                    target_task = tasks_by_w[p]
                    #check if any crane that can perform for this task
                    available_cranes=Array{Tuple{Int, Number}, 1}()
                    for q in TIME.available_cranes
                        #randomly select the cranes based on travel time
                        if target_task.b in QC[q].available_bays
                            if (target_task.b-QC[q].current_bay) == 0
                                available_cranes = [(q, 1)]
                                break
                            else
                                if crit == "dist"
                                    push!(available_cranes, (q, (abs(target_task.b-QC[q].current_bay)^2)/CTS.J*rand()))
                                else
                                    push!(available_cranes, (q, (abs(target_task.b-init_bay(q, CTS)^2)*rand())))
                                end
                                #push!(available_cranes, (q, (abs(target_task.b-QC[q].current_bay)^2)/CTS.J))
                                #push!(available_cranes, (q, (abs(target_task.b-init_bay(q, CTS)^2))))
                            end
                        end
                    end
                    if length(available_cranes) == 0
                        break
                    end

                    #try to add task if there are no clearance problems
                    for q in sort(available_cranes, by=last)
                        if check_clearance(QC, q[1], target_bay, CTS) == true && length(QC[q[1]].task_buffer) == 0
                            add_task(target_task, q[1], TIME, LS, QC, CTS)
                            return("New task successfuly added. \n---------------")
                        end
                    end

                    for q in available_cranes
                        (check, move_q, move_bay, move_status) = check_double_clearance(QC, q[1], target_bay, CTS)
                        if  check == true && length(QC[q[1]].task_buffer) == 0
                            add_task_move(target_task, q[1], move_q, move_bay, move_status, LS, TIME, QC, CTS)
                            return("New task + neighbour move successfuly added. \n---------------")
                        end
                    end

                    for q in available_cranes
                        (check, move_q, move_bay, move_q2, move_bay2) = check_triple_clearance(QC, q[1], target_bay, CTS)
                        if  check == true && length(QC[q[1]].task_buffer) == 0
                            add_move(move_q2, move_bay2, LS, TIME, QC, CTS)
                            add_task_move(target_task, q[1], move_q, move_bay, "idle", LS, TIME, QC, CTS)
                            return("New task + neighbour move successfuly added. \n---------------")
                        end
                    end
                end
            end
        end
    end
    for q in QC
        if q.status != "idle"
            return("Next time period")
        end
    end
    if CTS.Q >= 4
        stucked_bays(order, tasks_by_w, prec, bj, LS, TIME, QC, CTS)
    end
    return("Next time period")
end

function randomizedConstructionHeuristic(ind::String, crit::String, tasks_by_w::Dict{Int, LTask}, prec::Dict{Int, Array}, bj::Array{Int, 1}, LS::LoadingSequence, TIME::Timer, QC::Array{QuayCrane, 1}, CTS::Constants)
    #check if all cranes are busy
    if length(TIME.available_cranes) == 0
        return("All cranes are busy")
    end
    #check if LS is completed
    if LS.tasks_left == 0
        return("LS is completed")
    end

    #order according to indicator
    work_load = order_by_maximal_bay(false, tasks_by_w, bj, LS, CTS)
    if ind == "dist"
        order = order_by_dist(true, work_load, QC, CTS)
    elseif ind == "number"
        work_load_dist = order_by_dist(false, work_load, QC, CTS)
        order = order_by_number(true, work_load_dist, QC, CTS)
    end

    output = ConstructionHeuristic(order, crit, tasks_by_w, prec, bj, LS, TIME, QC, CTS)
    return(output)
end

function deterministicConstructionHeuristic(ind::String, crit::String, tasks_by_w::Dict{Int, LTask}, prec::Dict{Int, Array}, bj::Array{Int, 1}, LS::LoadingSequence, TIME::Timer, QC::Array{QuayCrane, 1}, CTS::Constants)
    #check if all cranes are busy
    if length(TIME.available_cranes) == 0
        return("All cranes are busy")
    end
    #check if LS is completed
    if LS.tasks_left == 0
        return("LS is completed")
    end

    #order according to indicator
    work_load = order_by_maximal_bay(false, tasks_by_w, bj, LS, CTS)
    if ind == "minimal"
        order = reverse!(work_load)
    elseif ind == "number"
        work_load_dist = order_by_dist(false, work_load, QC, CTS)
        order = order_by_number(false, work_load_dist, QC, CTS)
    end

    output = ConstructionHeuristic(order, crit, tasks_by_w, prec, bj, LS, TIME, QC, CTS)
    return(output)
end

function stucked_bays(order::Array{NamedTuple{(:j, :prob),Tuple{Int, Float64}}, 1}, tasks_by_w::Dict{Int, LTask}, prec::Dict{Int, Array}, bj::Array{Int, 1}, LS::LoadingSequence, TIME::Timer, QC::Array{QuayCrane, 1}, CTS::Constants)
    if LS.tasks_left != 0
        for tuple in order
            target_bay = tuple.j
            #select a task within the target_bay
            for p in bay_to_pos(target_bay, bj)
                if check_prec(LS, tasks_by_w[p], prec) == true
                    if check_loaded_filled(LS, tasks_by_w[p]) == true
                        target_task = tasks_by_w[p]
                        #check if any crane that can perform for this task
                        if target_bay in QC[1].available_bays && 1 in TIME.available_cranes
                            add_move(3, target_bay + CTS.delta + 3, LS, TIME, QC, CTS)
                            add_move(4, target_bay + CTS.delta + 5, LS, TIME, QC, CTS)
                            add_task_move(target_task, 1, 2, target_bay + CTS.delta + 1, "idle", LS, TIME, QC, CTS)
                            return("New task successfuly added. \n---------------")
                        elseif target_bay in QC[CTS.Q].available_bays && CTS.Q in TIME.available_cranes
                            add_move(2, target_bay - CTS.delta - 3, LS, TIME, QC, CTS)
                            add_move(1, target_bay - CTS.delta - 5, LS, TIME, QC, CTS)
                            add_task_move(target_task, 4, 3, target_bay - CTS.delta - 1, "idle", LS, TIME, QC, CTS)
                            return("New task successfuly added. \n---------------")
                        end
                    end
                end
            end
        end
        return("Next time period")
    else
        return("LS is completed")
    end
end


#=
The main module for Femshop.
=#
module Femshop

# Public macros and functions
export @generateFor, @domain, @mesh, @solver, @stepper, @setSteps, @functionSpace, @trialFunction, @matrixFree,
        @testFunction, @nodes, @order, @boundary, @addBoundaryID, @referencePoint, @variable, @coefficient, @parameter, @testSymbol, @initial,
        @timeInterval, @weakForm, @fluxAndSource, @LHS, @RHS, @exportCode, @importCode,
        @customOperator, @customOperatorFile,
        @outputMesh, @useLog, @finalize
export init_femshop, set_language, set_custom_gen_target, dendro, set_solver, set_stepper, set_specified_steps, set_matrix_free,
        reformat_for_stepper, reformat_for_stepper_fv,
        add_mesh, output_mesh, add_boundary_ID, add_test_function,
        add_initial_condition, add_boundary_condition, add_reference_point, set_rhs, set_lhs, set_lhs_surface, set_rhs_surface,
        set_symexpressions, set_assembly_loops
export build_cache_level, build_cache, build_cache_auto
export sp_parse
export generate_code_layer, generate_code_layer_surface, generate_code_layer_fv
export Variable, add_variable
export Coefficient, add_coefficient
export Parameter, add_parameter
export FVInfo

### Module's global variables ###
# configuration, output files, etc.
config = nothing;
prob = nothing;
project_name = "unnamedProject";
output_dir = pwd();
language = 0;
gen_framework = 0;
generate_external = false;
solver = nothing;
codegen_params = nothing;
#log
use_log = false;
log_file = "";
log_line_index = 1;
#mesh
mesh_data = nothing;    # The basic element information as read from a MSH file or generated here.
grid_data = nothing;    # The full collection of nodes(including internal nodes) and other mesh info in the actual DOF ordering.
geo_factors = nothing;  # Geometric factors
fv_info = nothing;      # Finite volume info
refel = nothing;        # Reference element (will eventually be an array of refels)
fv_refel = nothing;        # Reference element for FV
elemental_order = [];   # Determines the order of the elemental loops
parent_maps = nothing;  # For high order FV
fv_grid = nothing;      # For high order FV
fv_geo_factors = nothing;# Geometric factors for FV
#problem variables
var_count = 0;
variables = [];
coefficients = [];
parameters = [];
test_functions = [];
indexers = [];
#generated functions
genfunc_count = 0;
genfunctions = [];
#rhs
linears = [];
face_linears = [];
#lhs
bilinears = [];
face_bilinears = [];
#assembly loop functions
assembly_loops = [];
#symbolic layer
symexpressions = [[],[],[],[]];
# string versions of generated code for printing
code_strings = [[],[],[],[]];
#time stepper
time_stepper = nothing;
specified_dt = 0;
specified_Nsteps = 0;
use_specified_steps = false;

use_cachesim = false;

#handles for custom code gen functions
custom_gen_funcs = [];

###############################################################
include("femshop_includes.jl");
include("macros.jl");
include("femshop_interface.jl"); # included here after globals are defined
###############################################################

config = Femshop_config(); # These need to be initialized here
prob = Femshop_prob();

# This has already been initialized on loading, so this is more of a reset function
function init_femshop(name="unnamedProject")
    global config = Femshop_config();
    global prob = Femshop_prob();
    global project_name = name;
    global language = JULIA;
    global framework = 0;
    global generate_external = false;
    global codegen_params = nothing;
    global log_file = "";
    global use_log = false;
    global log_line_index = 1;
    global mesh_data = nothing;
    global grid_data = nothing;
    global geo_factors = nothing;
    global fv_info = nothing;
    global refel = nothing;
    global fv_refel = nothing;
    global elemental_order = [];
    global parent_maps = nothing;
    global fv_grid = nothing;
    global fv_geo_factors = nothing;
    global var_count = 0;
    global variables = [];
    global coefficients = [];
    global parameters = [];
    global test_functions = [];
    global indexers = [];
    global genfunc_count = 0;
    global genfunctions = [];
    global linears = [];
    global bilinears = [];
    global face_linears = [];
    global face_bilinears = [];
    global assembly_loops = [];
    global symexpressions = [[],[],[],[]];
    global code_strings = [[],[],[],[]];
    global time_stepper = nothing;
    global specified_dt = 0;
    global specified_Nsteps = 0;
    global use_specified_steps = false;
    global use_cachesim = false;
end

# Sets the code generation target
function set_included_gen_target(lang, framework, dirpath, name; head="")
    #TODO
    println("Premade generation targets currently under construction. Sorry. Reverting to Julia target.");
    return;

    global language = lang;
    global gen_framework = framework;
    global output_dir = dirpath;
    global project_name = name;
    global generate_external = true;
    init_codegenerator(dirpath, name, head);

    # Need to set these three functions

    set_generation_target(get_external_language_elements, generate_external_code_layer, generate_external_files);
end

# Setting a custom target requires three functions
# 1. get_external_language_elements() - file extensions, comment chars etc.
# 2. generate_external_code_layer(var, entities, terms, lorr, vors) - Turns symbolic expressions into code
# 3. generate_external_files(var, lhs_vol, lhs_surf, rhs_vol, rhs_surf) - Writes all files based on generated code
function set_custom_gen_target(lang_elements, code_layer, file_maker, dirpath, name; head="", params=nothing)
    global language = CUSTOM_GEN_TARGET;
    global gen_framework = CUSTOM_GEN_TARGET;
    global output_dir = dirpath;
    global project_name = name;
    global generate_external = true;
    init_code_generator(dirpath, name, head);
    set_generation_target(lang_elements, code_layer, file_maker);
end

# Set parameters used by code generation. This is specific to the target.
function set_codegen_parameters(params)
    global codegen_params = params;
end

# Set the solver module and type
function set_solver(stype)
    config.solver_type = stype;
    if stype == DG
        global solver = DGSolver;
    elseif stype == CG
        global solver = CGSolver;
    elseif stype == FV
        global solver = FVSolver;
        sourcedir = @__DIR__
        add_custom_op_file(string(sourcedir)*"/fv_ops.jl");
    end
end

# Sets the time stepper
function set_stepper(type, cfl)
    global time_stepper = Stepper(type, cfl);
    global config.stepper = type;
    log_entry("Set time stepper to "*type, 1);
end

# Time steps can be chosen automatically or specified. This is for manual specifying.
function set_specified_steps(dt, steps)
    global specified_dt = dt;
    global specified_Nsteps = steps;
    global use_specified_steps = true;
    prob.time_dependent = true;
    prob.end_time = dt*steps;
    log_entry("Set time stepper values to dt="*string(dt)*", Nsteps="*string(steps), 1);
end

# Uses matrix free iterative solver with the given max iterations and error tolerance.
function set_matrix_free(max, tol)
    config.linalg_matrixfree = true;
    config.linalg_matfree_max = max;
    config.linalg_matfree_tol = tol;
end

# Adds a mesh and builds the full grid and reference elements.
# Actually, the mesh object passed here could be either just a MeshData struct,
# or a tuple of MeshData, Refel and Grid already built.
function add_mesh(mesh)
    if typeof(mesh) <: Tuple
        global mesh_data = mesh[1];
        global refel = mesh[2];
        global grid_data = mesh[3];

    else
        global mesh_data = mesh;
        global refel;
        global grid_data;
        (refel, grid_data) = grid_from_mesh(mesh_data);

    end

    constantJ = true;
    do_faces = false;
    do_vol = false;
    if config.solver_type == DG || config.solver_type == FV
        do_faces = true;
    end
    if config.solver_type == FV
        do_vol = true;
    end

    global geo_factors = build_geometric_factors(refel, grid_data, do_face_detj=do_faces, do_vol_area=do_vol, constant_jacobian=constantJ);

    log_entry("Added mesh with "*string(mesh_data.nx)*" vertices and "*string(mesh_data.nel)*" elements.", 1);
    log_entry("Full grid has "*string(size(grid_data.allnodes,2))*" nodes.", 2);

    # If using FV, set up the FV_info
    if config.solver_type == FV
        global fv_info = build_FV_info(grid_data);
    end

    # Set elemental loop ordering to match the order from mesh for now.
    global elemental_order = 1:mesh_data.nel;
end

# Write the mesh to a MSH file
function output_mesh(file, format)
    write_mesh(file, format, mesh_data);
    log_entry("Wrote mesh data to file.", 1);
end

function set_parent_and_child(p_maps, c_grid)
    global parent_maps = p_maps;
    global fv_grid = c_grid;
    dim = config.dimension;
    nfaces = size(fv_grid.element2face,1);
    global fv_refel = refel = build_refel(dim, 1, nfaces, config.elemental_nodes);
    global fv_geo_factors = build_geometric_factors(fv_refel, fv_grid, do_face_detj=true, do_vol_area=true, constant_jacobian=true);
    global fv_info = build_FV_info(fv_grid);
    log_entry("FCreated parent-child grid with "*string(size(c_grid.allnodes,2))*" nodes and "*string(size(c_grid.loc2glb,2))*" elements.", 2);

    # If CELL variables exist, resize their values
    N = size(fv_grid.loc2glb, 2);
    for i=1:length(variables)
        if variables[i].location == CELL
            if var.type == SCALAR
                val_size = (1, N);
            elseif var.type == VECTOR
                val_size = (config.dimension, N);
            elseif var.type == TENSOR
                val_size = (config.dimension*config.dimension, N);
            elseif var.type == SYM_TENSOR
                val_size = (Int((config.dimension*(config.dimension+1))/2), N);
            elseif var.type == VAR_ARRAY
                if typeof(var.indexer) <: Array
                    comps = 1;
                    for j=1:length(var.indexer)
                        comps *= length(var.indexer[j].range);
                    end
                elseif typeof(var.indexer) == Indexer
                    comps = length(var.indexer.range);
                else
                    comps = 1;
                end
                val_size = (comps, N);
            end

            variables[i].values = zeros(val_size);
        end
    end
end

# Maybe remove. This is in grid.jl
function add_boundary_ID(bid, on_bdry)
    add_boundary_ID_to_grid(bid, on_bdry, grid_data);
end

# Defines a test function symbol by creating a special coefficient object.
function add_test_function(v, type)
    varind = length(test_functions) + 1;
    # make SymType
    dim = config.dimension;
    components = 1;
    if type == SCALAR
        components = 1;
    elseif type == VECTOR
        components = dim;
    elseif type == TENSOR
        components = dim*dim;
    elseif type == SYM_TENSOR
        if dim == 1
            components = 1;
        elseif dim == 2
            components = 3;
        elseif dim == 3
            components = 6;;
        elseif dim == 4
            components = 10;
        end
    elseif type == VAR_ARRAY
        components = 1;
    end
    symvar = sym_var(string(v), type, components);

    push!(test_functions, Femshop.Coefficient(v, symvar, varind, type, NODAL, []););
    log_entry("Set test function symbol: "*string(v)*" of type: "*type, 2);
end

# Adds a variable and allocates everything associated with it.
function add_variable(var)
    global var_count += 1;

    # adjust values arrays
    if grid_data === nothing
        N = 1;
    else
        if var.location == CELL
            if fv_grid === nothing
                N = size(grid_data.loc2glb, 2);
            else
                N = size(fv_grid.loc2glb, 2);
            end
        else
            N = size(grid_data.allnodes,2);
        end
    end

    if var.type == SCALAR
        val_size = (1, N);
        comps = 1;
    elseif var.type == VECTOR
        val_size = (config.dimension, N);
        comps = config.dimension;
    elseif var.type == TENSOR
        val_size = (config.dimension*config.dimension, N);
        comps = config.dimension*config.dimension;
    elseif var.type == SYM_TENSOR
        val_size = (Int((config.dimension*(config.dimension+1))/2), N);
        comps = val_size[1];
    elseif var.type == VAR_ARRAY
        if typeof(var.indexer) <: Array
            comps = 1;
            for i=1:length(var.indexer)
                comps *= length(var.indexer[i].range);
            end
        elseif typeof(var.indexer) == Indexer
            comps = length(var.indexer.range);
        else
            comps = 1;
        end
        val_size = (comps, N);
    end
    var.total_components = comps;

    if language == JULIA || language == 0
        var.values = zeros(val_size);
    end

    # make symbolic layer variable symbols
    var.symvar = sym_var(string(var.symbol), var.type, var.total_components);

    global variables = [variables; var];

    global linears = [linears; nothing];
    global bilinears = [bilinears; nothing];
    global face_linears = [face_linears; nothing];
    global face_bilinears = [face_bilinears; nothing];
    global assembly_loops = [assembly_loops; nothing];
    global symexpressions[1] = [symexpressions[1]; nothing];
    global symexpressions[2] = [symexpressions[2]; nothing];
    global symexpressions[3] = [symexpressions[3]; nothing];
    global symexpressions[4] = [symexpressions[4]; nothing];
    global code_strings[1] = [code_strings[1]; ""];
    global code_strings[2] = [code_strings[2]; ""];
    global code_strings[3] = [code_strings[3]; ""];
    global code_strings[4] = [code_strings[4]; ""];

    log_entry("Added variable: "*string(var.symbol)*" of type: "*var.type*", location: "*var.location, 2);
end

# Adds a coefficient with either constant value or some generated function of (x,y,z,t)
function add_coefficient(c, type, location, val, nfuns)
    global coefficients;
    vals = [];
    if nfuns == 0 # constant values
        vals = val;
        if length(vals) == 1 && !(typeof(vals) <: Array)
            vals = [val];
        end

    else # genfunction values
        if typeof(val) <: Array
            ind = length(genfunctions) - nfuns + 1;
            for i=1:length(val)
                if typeof(val[i]) == String
                    push!(vals, genfunctions[ind]);
                    ind += 1;
                else
                    push!(vals, val[i]);
                end
            end
        else
            push!(vals, genfunctions[end]);
        end
    end
    components = length(vals);

    symvar = sym_var(string(c), type, components);

    index = length(coefficients);
    push!(coefficients, Coefficient(c, symvar, index, type, location, vals));

    log_entry("Added coefficient "*string(c)*" : "*string(val), 2);

    return coefficients[end];
end

# Adds a parameter entity.
function add_parameter(p, type, val)
    index = length(parameters);
    push!(parameters, Parameter(p, index, type, val));

    log_entry("Added parameter "*string(p)*" : "*string(val), 2);

    return parameters[end];
end

# Adds an Indexer entity
function add_indexer(indexer)
    push!(indexers, indexer);
    if length(indexer.range) < 3
        log_entry("Added indexer "*string(indexer.symbol)*" : "*string(indexer.range));
    else
        log_entry("Added indexer "*string(indexer.symbol)*" : ["*string(indexer.range[1])*", ... "*string(indexer.range[end])*"]");
    end

end

# Needed to insert parameter expressions into the weak form expressions.
function swap_parameter_xyzt(ex)
    if typeof(ex) == Symbol
        if ex === :x
            return :parameterCoefficientForx ;
        elseif ex === :y
            return :parameterCoefficientFory ;
        elseif ex === :z
            return :parameterCoefficientForz ;
        elseif ex === :t
            return :parameterCoefficientFort ;
        end
    elseif typeof(ex) == Expr
        for i=1:length(ex.args)
            ex.args[i] = swap_parameter_xyzt(ex.args[i]); # Recursively swap
        end
    end
    return ex;
end

# Sets initial condition for the given variable, but does not initialize.
function add_initial_condition(varindex, ex, nfuns)
    global prob;
    while length(prob.initial) < varindex
        prob.initial = [prob.initial; nothing];
    end
    if typeof(ex) <: Array
        vals = [];
        ind = length(genfunctions) - nfuns + 1;
        for i=1:length(ex)
            if typeof(ex[i]) == String
                push!(vals, genfunctions[ind]);
                ind += 1;
            else
                push!(vals, ex[i]);
            end
        end
        prob.initial[varindex] = vals;
    else
        if typeof(ex) == String
            prob.initial[varindex] = genfunctions[end];
        else
            prob.initial[varindex] = ex;
        end
    end

    log_entry("Initial condition for "*string(variables[varindex].symbol)*" : "*string(prob.initial[varindex]), 2);
    # hold off on initializing till solve or generate is determined.
end

# Sets boundary condition for a variable and BID.
function add_boundary_condition(var, bid, type, ex, nfuns)
    global prob;
    # make sure the arrays are big enough
    if size(prob.bc_func)[1] < var_count || size(prob.bc_func,2) < bid
        if !(grid_data===nothing)
            nbid = length(grid_data.bids);
        else
            # For some targets no grid is created
            nbid = 1;
        end

        tmp1 = Array{String,2}(undef, (var_count, nbid));
        tmp2 = Array{Any,2}(undef, (var_count, nbid));
        tmp3 = zeros(Int, (var_count, nbid));
        fill!(tmp1, "");
        fill!(tmp2, GenFunction("","","",0,0));
        tmp1[1:size(prob.bc_func,1), 1:size(prob.bc_func,2)] = Base.deepcopy(prob.bc_type);
        tmp2[1:size(prob.bc_func,1), 1:size(prob.bc_func,2)] = Base.deepcopy(prob.bc_func);
        tmp3[1:size(prob.bc_func,1), 1:size(prob.bc_func,2)] = prob.bid;
        prob.bc_type = tmp1;
        prob.bc_func = tmp2;
        prob.bid = tmp3;
    end
    if typeof(ex) <: Array
        vals = [];
        ind = length(genfunctions) - nfuns + 1;
        for i=1:length(ex)
            if typeof(ex[i]) == String
                push!(vals, genfunctions[ind]);
                ind += 1;
            else
                push!(vals, ex[i]);
            end
        end
        prob.bc_func[var.index, bid] = vals;
    else
        if typeof(ex) == String
            prob.bc_func[var.index, bid] = [genfunctions[end]];
        else
            prob.bc_func[var.index, bid] = [ex];
        end
    end
    prob.bc_type[var.index, bid] = type;
    prob.bid[var.index, bid] = bid;

    log_entry("Boundary condition: var="*string(var.symbol)*" bid="*string(bid)*" type="*type*" val="*string(ex), 2);
end

# A reference point is a single point with a defined value.
# It's treated like a Dirichlet boundary with one boundary node.
function add_reference_point(var, pos, val)
    global prob;
    # make sure the array is big enough
    if size(prob.ref_point,1) < var_count
        tmp = Array{Any,2}(undef, (var_count, 3));
        for i=1:size(prob.ref_point,1)
            tmp[i,:] = prob.ref_point[i,:];
        end
        for i=(size(prob.ref_point,1)+1):var_count
            tmp[i,1] = false;
            tmp[i,2] = [0,0];
            tmp[i,3] = [0];
        end
        prob.ref_point = tmp;
    end
    if typeof(pos) <: Number
        pos = pos*ones(config.dimension);
    end
    if typeof(val) <: Number
        val = val*ones(length(var.symvar));
    end

    # Find the closest vertex to pos
    # The stored pos is actually the index into glbvertex pointing to the closest vertex
    ind = [1,1];
    mindist = 12345;
    for ei=1:mesh_data.nel
        for i=1:size(grid_data.glbvertex,1)
            d = 0;
            for comp=1:length(pos)
                d = d + abs(grid_data.allnodes[comp, grid_data.glbvertex[i, ei]] - pos[comp]);
            end
            if d<mindist
                ind = [i,ei];
                mindist = d;
            end
        end
    end

    prob.ref_point[var.index, 1] = true;
    prob.ref_point[var.index, 2] = ind;
    prob.ref_point[var.index, 3] = val;

    log_entry("Reference point: var="*string(var.symbol)*" position="*string(pos)*" value="*string(val), 2);
end

# Sets the RHS(linear) function for the given variable(s).
function set_rhs(var, code="")
    global linears;
    if language == 0 || language == JULIA
        if typeof(var) <:Array
            for i=1:length(var)
                linears[var[i].index] = genfunctions[end];
                code_strings[2][var[i].index] = code;
            end
        else
            linears[var.index] = genfunctions[end];
            code_strings[2][var.index] = code;
        end

    else # external generation
        if typeof(var) <:Array
            for i=1:length(var)
                linears[var[i].index] = code;
            end
        else
            linears[var.index] = code;
        end
    end
end

# Sets the LHS(bilinear) function for the given variable(s).
function set_lhs(var, code="")
    global bilinears;
    if language == 0 || language == JULIA
        if typeof(var) <:Array
            for i=1:length(var)
                bilinears[var[i].index] = genfunctions[end];
                code_strings[1][var[i].index] = code;
            end
        else
            bilinears[var.index] = genfunctions[end];
            code_strings[1][var.index] = code;
        end

    else # external generation
        if typeof(var) <:Array
            for i=1:length(var)
                bilinears[var[i].index] = code;
            end
        else
            bilinears[var.index] = code;
        end
    end
end

# Sets the surface RHS(linear) function for the given variable(s).
function set_rhs_surface(var, code="")
    global face_linears;
    if language == 0 || language == JULIA
        if typeof(var) <:Array
            for i=1:length(var)
                face_linears[var[i].index] = genfunctions[end];
                code_strings[4][var[i].index] = code;
            end
        else
            face_linears[var.index] = genfunctions[end];
            code_strings[4][var.index] = code;
        end

    else # external generation
        if typeof(var) <:Array
            for i=1:length(var)
                face_linears[var[i].index] = code;
            end
        else
            face_linears[var.index] = code;
        end
    end
end

# Sets the surface LHS(bilinear) function for the given variable(s).
function set_lhs_surface(var, code="")
    global face_bilinears;
    if language == 0 || language == JULIA
        if typeof(var) <:Array
            for i=1:length(var)
                face_bilinears[var[i].index] = genfunctions[end];
                code_strings[3][var[i].index] = code;
            end
        else
            face_bilinears[var.index] = genfunctions[end];
            code_strings[3][var.index] = code;
        end

    else # external generation
        if typeof(var) <:Array
            for i=1:length(var)
                face_bilinears[var[i].index] = code;
            end
        else
            face_bilinears[var.index] = code;
        end
    end
end

function set_symexpressions(var, ex, lorr, vors)
    # If the ex is empty, don't set anything
    if ex == [] || ex == [[]]
        return;
    end

    if typeof(var) <:Array
        for i=1:length(var)
            set_symexpressions(var[i], ex, lorr, vors);
        end

    else
        global symexpressions;
        if lorr == LHS
            if vors == "volume"
                ind = 1;
            else
                ind = 2;
            end
        else # rhs
            if vors == "volume"
                ind = 3;
            else
                ind = 4;
            end
        end
        symexpressions[ind][var.index] = ex;
    end
end

function set_assembly_loops(var, code="")
    global assembly_loops;
    if language == 0 || language == JULIA
        if typeof(var) <:Array
            for i=1:length(var)
                assembly_loops[var[i].index] = genfunctions[end];
            end
        else
            assembly_loops[var.index] = genfunctions[end];
        end

    else # external generation
        if typeof(var) <:Array
            for i=1:length(var)
                assembly_loops[var[i].index] = code;
            end
        else
            assembly_loops[var.index] = code;
        end
    end
end

function finalize()
    finalize_femshop();
end

end # module


#= Rendering Monetary values =#

# render functions
function loweramount(spec::FormatSpecification, m::Monetary)
    negfs = get(spec, ParenthesizeNegative, nothing)
    if sign(m) < 0
        if negfs == nothing
            [:symbefore, :minus_sign, :amount, :symafter]
        elseif negfs.symloc == :inside
            ["(", :symbefore, :amount, :symafter, ")"]
        else
            [:symbefore, "(", :amount, ")", :symafter]
        end
    elseif sign(m) == 0 && negfs != nothing
        [:symbefore, :zero_dash, :symafter]
    else
        [:symbefore, :amount, :symafter]
    end
end

function resolve_symbol(symtype::Symbol, m::Monetary)
    if symtype == :short
        shortsymbol(m)
    elseif symtype == :long
        longsymbol(m)
    else
        iso4217alpha(m)
    end
end

"""
Compile the symbol requirement to a simplified form.

In the simplified form, the `location` field is guaranteed to take the value of
`:before` or `:after`.
"""
function compile(symreq::CurrencySymbol, m::Monetary)
    loc = if symreq.location == :dependent
        get(LOCAL_SYMBOL_LOCATION, currency(m), :after)
    elseif symreq.location == :before
        :before
    else  # :unspecified or :after
        :after
    end
    CurrencySymbol(
        symreq.symtype,
        loc,
        symreq.spacing,
        symreq.glued,
        symreq.compose)
end

function symbolize(template::Vector, spec::FormatSpecification, m::Monetary)
    symreq = compile(get(spec, CurrencySymbol, CurrencySymbol()), m)
    desired_symbol = foldl(
        |>, symreq.compose; init=resolve_symbol(symreq.symtype, m))
    spacing = symreq.spacing == :none ? "" : :thin_space

    next_template = []
    for item in template
        if item == :symbefore
            if symreq.location == :before && symreq.glued != :require
                push!(next_template, desired_symbol, spacing)
            end
        elseif item == :symafter
            if symreq.location ∈ (:after, :dependent, :unspecified) &&
                symreq.glued != :require
                push!(next_template, spacing, desired_symbol)
            end
        elseif item == :amount && symreq.glued == :require
            if symreq.location == :before
                push!(next_template, desired_symbol, spacing, item)
            elseif symreq.location == :after
                push!(next_template, item, spacing, desired_symbol)
            end
        else
            push!(next_template, item)
        end
    end
    next_template
end

function render(template::Vector, spec::FormatSpecification, m::Monetary)
    decisep = get(spec, DecimalSeparator, DecimalSeparator("."))
    digisep = get(spec, DigitSeparator, DigitSeparator(""))

    prec = decimals(m)
    intpart = trunc(BigInt, abs(m.val))
    floatpart = round(BigInt, big(10)^prec * (abs(m.val) - intpart))

    syms = getsymboltable(spec)
    next_template = []
    for item in template
        if item == :amount
            push!(next_template, digitseparate(intpart, digisep))
            if prec != 0
                push!(next_template, decisep.sep, lpad(floatpart, prec, '0'))
            end
        elseif haskey(syms, item)
            push!(next_template, syms[item])
        else push!(next_template, item) end
    end
    next_template
end

"""
    format(m::Monetary; styles=[:finance])

Format the given monetary amount to meet the requirements of the given style.
Available styles are: `:finance`, `:us`, `:european`, and `:brief`. For LaTeX
output, provide `:latex`. For plain output (default), provide `:plain`.
"""
function format(m::Monetary; styles=[:finance])
    specs = map(x -> REQUIREMENTS[x], styles)
    reqs = reduce(∪, specs)
    format(m, reqs)
end

function format(m::Monetary, spec::FormatSpecification)
    template = loweramount(spec, m)
    template = symbolize(template, spec, m)
    template = render(template, spec, m)
    join(template) |> String
end


module VennDiagrams

using Compose
using Color

function venn(xs::Union(AbstractArray,Set)...; proportional::Bool = true, labels=Union(Bool,Vector{String}), colors=Union(Bool,Vector{ColorValue},Vector{AlphaColorValue}))
    n = length(xs)

    if eltype(xs) != Set
        cols = map(Set, xs)
    else
        cols = xs
    end

    if proportional
        error("Not implemented yet!")
        #sizes = 0
    end

    return p
end

end # module


using GeometryTypes

abstract type TrialState end

mutable struct FixationState <: TrialState
    area::Rectangle
    clock::ClockTimer
    current_pos::Point2f0
end

mutable struct TargetState <: TrialState
    area::Rectangle
    clock::ClockTimer
    current_pos::Point2f0
end

mutable struct ResponseState <: TrialState
    area::Rectangle
    clock::ClockTimer
    current_pos::Point2f0
end

mutable struct TrialEndState <: TrialState
    clock::ClockTimer
end

mutable struct StateTransitions
    states::Vector{TrialState}
    edges::Matrix{Int64}
    current::Int64
end

mutable struct ExperimentRecord
    clock::Clock
    timestamp::Vector{Float64}
    event::Vector{String}
end

ExperimentRecord() = ExperimentRecord(Clock(), Float64[], String[])

function next!(tt::StateTransitions,engaged::Bool, record::ExperimentRecord)
    nn = tt.current
    if engaged
        nn = tt.edges[1,tt.current]
    else
        nn = tt.edges[2, tt.current]
    end
    #log this event
    t1 = get_time(record.clock)
    push!(record.timestamp, t1)
    push!(record.event, "$(tt.current) -> $(nn)")
    #make sure we reset the clock
    reset!(tt.states[nn].clock)
    tt.current = nn
    tt.current
end

check_state(state::TrialState) = error("Not implemented")
next(state::TrialState, engaged::Bool) = error("Not implemented")
get_position(state::TrialState) = state.current_pos
get_area(state::TrialState) = state.area
get_area(state::TrialEndState) = Rectangle(-Inf32, -Inf32, Inf32, Inf32)

function set_position!(state::TrialState, pos::Point2f0)
    state.current_pos = pos
    nothing
end
set_position!(state::TrialEndState,pos::Point2f0) = nothing

function next!(transitions::StateTransitions, record::ExperimentRecord)
    state = transitions.states[transitions.current]
    b = check_state(state)
    #check if we are transitioning and if so to which state
    if b == 1
        next!(transitions, true, record)
    elseif b == 2
        next!(transitions, false, record)
    end
    nothing
end

"""
Check whether we should transition out of `state`. Returns 0 if we should stay in the same state, 1 if we should transition to the next valid state and 2 if we should transition to the next invalid state.
"""
function check_state(state::Union{FixationState,ResponseState})
    b = 0
    if is_inside(state)
        if is_done!(state.clock)
            b = 1
        end
    else
        b = 2
    end
    b
end

function check_state(state::TrialEndState)
    b = 0
    if is_done!(state.clock)
        b = 1
    end
    b
end

function is_inside(state::TrialState)
    pos = get_position(state)
    is_inside(state, pos)
end

function is_inside(state::TrialState, pos::Point2f0)
    area = get_area(state)
    b = (area.x <= pos[1] <= area.x + area.w)
    b &= (area.y <= pos[2] <= area.y + area.h)
    return b
end


"""
Struct for a Libxc functional and some basic information
"""
mutable struct Functional
    identifier::Symbol
    n_spin::Int
    name::String
    kind::Symbol
    family::Symbol
    flags::Vector{Symbol}
    derivatives::Vector{Int}  # Derivatives supported by the functional
    references::Vector  # Related article references as list of named tuples

    # Spin dimensions libxc expects for various quantities
    # Note: Only the symbols actually meaningful for this functional
    # contain valid information
    spin_dimensions

    # Pointer holding the Libxc representation of this functional
    pointer_::Ptr{xc_func_type}
end

"""
    Functional(identifier::Symbol; n_spin::Integer = 1)

Construct a Functional from a libxc `identifier` and the number
of spins `n_spin` to consider. `
"""
function Functional(identifier::Symbol; n_spin::Integer = 1)
    n_spin in (1, 2) || error("n_spin needs to be 1 or 2")

    number = xc_functional_get_number(string(identifier))
    number == -1 && error("Functional $identifier is not known.")

    function pointer_cleanup(ptr::Ptr{xc_func_type})
        if ptr != C_NULL
            xc_func_end(ptr)
            xc_func_free(ptr)
        end
    end

    pointer = xc_func_alloc()
    ret = xc_func_init(pointer, number, n_spin)
    ret != 0 && error("Something went wrong initialising the functional")

    try
        funcinfo = xc_func_get_info(pointer)
        kind     = KINDMAP[xc_func_info_get_kind(funcinfo)]
        family   = FAMILIYMAP[xc_func_info_get_family(funcinfo)]
        flags    = extract_flags(xc_func_info_get_flags(funcinfo))
        name     = unsafe_string(xc_func_info_get_name(funcinfo))
        references = extract_references(funcinfo)
        dimensions = unsafe_load(pointer).dim

        # Flags for having 0th to 4th derivative
        derivative_flags = (:exc, :vxc, :fxc, :kxc, :lxc)
        derivatives = [i-1 for (i, flag) in enumerate(derivative_flags) if flag in flags]

        # Make functional and attach finalizer for cleaning up the pointer
        func = Functional(identifier, n_spin, name, kind, family, flags, derivatives,
                          references, dimensions, pointer)
        finalizer(cls -> pointer_cleanup(cls.pointer_), func)
        return func
    catch
        pointer_cleanup(pointer)
        rethrow()
    end
end

"""
Return the list of derivatives supported by the functional
with `0` denoting the energy, `1` the potential, `2` the kernel and so on.
"""
supported_derivatives(func::Functional) = func.derivatives

"""Is the functional an LDA or hybrid LDA functional"""
is_lda(func::Functional)    = func.family in (:lda, :hyb_lda)

"""Is the functional a GGA or hybrid GGA functional"""
is_gga(func::Functional)    = func.family in (:gga, :hyb_gga)

"""Is the functional a meta-GGA or hybrid meta-GGA functional"""
is_mgga(func::Functional)   = func.family in (:mgga, :hyb_mgga)

"""Is the functional a hybrid functional (global or range-separated)"""
is_hybrid(func::Functional) = func.family in (:hyb_gga, :hyb_lda, :hyb_mgga)

"""Is the functional a VV10-type non-local density functional"""
is_vv10(func::Functional)   = :vv10 in func.flags

"""Is the functional a range-separated hybrid functional"""
function is_range_separated(func::Functional)
    is_hybrid(func) || return false
    any(flag in (:hyb_cam, :hyb_camy, :hyb_lc, :hyb_lcy) for flag in func.flags)
end

"""Is the functional a global hybrid functional"""
is_global_hybrid(func::Functional) = is_hybrid(func) && !is_range_separated(func)

"""Does the function need the laplacian of the density"""
needs_laplacian(func::Functional) = :needs_laplacian in func.flags

# Supported properties:
#     - density_threshold  Density threshold below which functional will not be evaluated
#     - zeta_threshold     Spin polarization threshold below which spin components
#                          will not be evaluated
#     - sigma_threshold    Enforced lower bound for σ = √γ, smaller values get overwritten
#     - tau_threshold      Enforced lower bound for τ, smaller values get overwritten
#     - exx_coefficient    Exact exchange for global hybrid functionals
#     - cam_omega          Range separation constant for range-separated hybrids
#     - cam_alpha          Long-range exact exchange for range-separated hybrids
#     - cam_beta           Additional exchange for short-range for range-separated hybrids
#                          (i.e. short-range exact exchange is cam_alpha + cam_beta)
#     - nlc_b              Parameter for the VV10-type functionals
#     - nlc_C              Parameter for the VV10-type functionals

import Base.getproperty, Base.setproperty!, Base.propertynames
function Base.propertynames(func::Functional, private::Bool=false)
    ret = [:density_threshold, :zeta_threshold, :sigma_threshold, :tau_threshold,
           :exx_coefficient, :cam_alpha, :cam_beta, :cam_omega, :nlc_b, :nlc_C]
    append!(ret, fieldnames(Functional))
end

function Base.getproperty(func::Functional, s::Symbol)
    if s == :density_threshold
        return Float64(unsafe_load(func.pointer_).dens_threshold)
    elseif s in (:zeta_threshold, :sigma_threshold, :tau_threshold)
        return Float64(getproperty(unsafe_load(func.pointer_), s))
    elseif s == :exx_coefficient
        return is_global_hybrid(func) ? Float64(xc_hyb_exx_coef(func.pointer_)) : nothing
    elseif s in (:cam_alpha, :cam_beta, :cam_omega)
        is_range_separated(func) || return nothing
        data = (cam_omega = Ref(0.0), cam_alpha = Ref(0.0), cam_beta  = Ref(0.0))
        xc_hyb_cam_coef(func.pointer_, data.cam_omega, data.cam_alpha, data.cam_beta)
        return getproperty(data, s)[]
    elseif s in (:nlc_b, :nlc_C)
        is_vv10(func) || return nothing
        data = (nlc_b=Ref(0.0), nlc_C=Ref(0.0))
        xc_nlc_coef(func.pointer_, data.nlc_b, data.nlc_C)
        return getproperty(data, s)[]
    else
        getfield(func, s)
    end
end

function Base.setproperty!(func::Functional, s::Symbol, v)
    setmap = Dict(
        :density_threshold => xc_func_set_dens_threshold,
        :zeta_threshold    => xc_func_set_zeta_threshold,
        :sigma_threshold   => xc_func_set_sigma_threshold,
        :tau_threshold     => xc_func_set_tau_threshold,
    )
    if s in keys(setmap)
        v < 0 && error("Threshold values cannot be negative.")
        setmap[s](func.pointer_, Cdouble(v))
    else
        error("Setting property $s is not allowed or implemented.")
    end
end

# Ideas:
#    https://github.com/psi4/psi4/blob/master/psi4/src/psi4/libfunctional/LibXCfunctional.cc
#    https://github.com/psi4/psi4/blob/master/psi4/src/psi4/libfunctional/LibXCfunctional.h
function external_parameter_defaults(func)
    params = Dict()
    n_extparams = xc_func_info_get_n_ext_params(func.pointer_)
    for i in 1:n_extparams
        name = xc_func_info_get_ext_params_name(func.pointer_, i - 1)
        params[unsafe_string(name)] = xc_func_info_get_ext_params_default_value(func.pointer_, i - 1)
    end
    params
end

# Provide maps which translate between the conventions
# used in Libxc.jl and libxc

const KINDMAP = Dict(
    XC_EXCHANGE             => :exchange,
    XC_CORRELATION          => :correlation,
    XC_EXCHANGE_CORRELATION => :exchange_correlation,
    XC_KINETIC              => :kinetic,
)

const FAMILIYMAP = Dict(
    XC_FAMILY_UNKNOWN   => :unknown,
    XC_FAMILY_LDA       => :lda,
    XC_FAMILY_GGA       => :gga,
    XC_FAMILY_MGGA      => :mgga,
    XC_FAMILY_LCA       => :lca,
    XC_FAMILY_OEP       => :oep,
    XC_FAMILY_HYB_GGA   => :hyb_gga,
    XC_FAMILY_HYB_MGGA  => :hyb_mgga,
    XC_FAMILY_HYB_LDA   => :hyb_lda,
)

const FLAGMAP = Dict(
    XC_FLAGS_HAVE_EXC        => :exc,
    XC_FLAGS_HAVE_VXC        => :vxc,
    XC_FLAGS_HAVE_FXC        => :fxc,
    XC_FLAGS_HAVE_KXC        => :kxc,
    XC_FLAGS_HAVE_LXC        => :lxc,
    XC_FLAGS_1D              => :dim1,
    XC_FLAGS_2D              => :dim2,
    XC_FLAGS_3D              => :dim3,
    XC_FLAGS_HYB_CAM         => :hyb_cam,
    XC_FLAGS_HYB_CAMY        => :hym_camy,
    XC_FLAGS_VV10            => :vv10,
    XC_FLAGS_HYB_LC          => :hyb_lc,
    XC_FLAGS_HYB_LCY         => :hyb_lcy,
    XC_FLAGS_STABLE          => :stable,
    XC_FLAGS_DEVELOPMENT     => :development,
    XC_FLAGS_NEEDS_LAPLACIAN => :needs_laplacian,
)

function extract_flags(flags)
    ret = Symbol[]
    for (flag, sym) in pairs(FLAGMAP)
        flag & flags > 0 && push!(ret, sym)
    end
    ret
end

function extract_references(info::Ptr{xc_func_info_type})
    # Range of set references
    refrange = filter(iref -> xc_func_info_get_references(info, iref) != C_NULL,
                      0:XC_MAX_REFERENCES - 1)

    map(refrange) do iref
        ref_ptr = xc_func_info_get_references(info, iref)
        (reference=unsafe_string(xc_func_reference_get_ref(ref_ptr)),
         doi=unsafe_string(xc_func_reference_get_doi(ref_ptr)),
         bibtex=unsafe_string(xc_func_reference_get_bibtex(ref_ptr)))
    end
end