using JuliaPBF.StructPBF

function GenParticles(inAnsDataStruct::AnalysisDataStruct)
    npart = inAnsDataStruct.nMaxParticles
    ar_ParticleData = Vector{ParticleData}(undef,npart)
    for ii in 1:npart
        ar_ParticleData[ii] = ParticleData(
                                inAnsDataStruct.nMaxParticles+1,  # gridID :: Int64
                                Vec2(),                           # vel    :: Vec2
                                Vec2(),                           # pos    :: Vec2
                                Vec2(),                           # temppos:: Vec2
                                0.0,                              # Lambda :: Float64
                                0.0,                              # mass   :: Float64
                                0.0)                              # phase  :: Int64
    end
    FillInitialBox(inAnsDataStruct, ar_ParticleData)
    return ar_ParticleData
end

function GenGrids(inAnsDataStruct::AnalysisDataStruct)
    start_point = inAnsDataStruct.analysisBox.staPoint
    end_point = inAnsDataStruct.analysisBox.endPoint
    grid_size = 2.0 * inAnsDataStruct.kernelRadius
    n_Grid_x = Int(ceil((end_point.x - start_point.x)/grid_size))
    n_Grid_y = Int(ceil((end_point.y - start_point.y)/grid_size))

    ini_nNearGrid      = Vector{Int64}(undef,n_Grid_x*n_Grid_y)
    ini_nGridParticles = Vector{Int64}(undef,n_Grid_x*n_Grid_y)
    ini_NearGridIndex  = Vector{Int64}(undef,9*(n_Grid_x-2)*(n_Grid_y-2) + 6*(2*(n_Grid_x-2)+2*(n_Grid_y-2)) + 4*4)
    nong = 0
    for iy in 1:n_Grid_y
        for ix in 1:n_Grid_x
            # 아랫줄
            if iy > 1
                if ix > 1
                    nong+=1
                    ini_NearGridIndex[nong] = ix-1 + (iy-2)*n_Grid_x
                end
                nong+=1
                ini_NearGridIndex[nong] = ix + (iy-2)*n_Grid_x
                if ix < n_Grid_x
                    nong+=1
                    ini_NearGridIndex[nong] = ix+1 + (iy-2)*n_Grid_x
                end
            end
            # 자기줄
            if ix > 1
                nong+=1
                ini_NearGridIndex[nong] = ix-1 + (iy-1)*n_Grid_x
            end
            nong+=1
            ini_NearGridIndex[nong] = ix + (iy-1)*n_Grid_x
            if ix < n_Grid_x
                nong+=1
                ini_NearGridIndex[nong] = ix+1 + (iy-1)*n_Grid_x
            end
            # 윗줄
            if iy < n_Grid_y
                if ix > 1
                    nong+=1
                    ini_NearGridIndex[nong] = ix-1 + iy*n_Grid_x
                end
                nong+=1
                ini_NearGridIndex[nong] = ix + iy*n_Grid_x
                if ix < n_Grid_x
                    nong+=1
                    ini_NearGridIndex[nong] = ix+1 + iy*n_Grid_x
                end
            end
            ii = ix + (iy-1)*n_Grid_x
            ini_nNearGrid[ii] = nong
            ini_nGridParticles[ii] = 0
        end
    end

    return GridData(ini_nNearGrid,ini_NearGridIndex,ini_nGridParticles)
end

function GenPhases(inAnsDataStruct::AnalysisDataStruct)
    ## CREATE BOUNDARY LINES ##
    # Boundary line is a object which shape never
    # change, but only 회전/병진. at this time, it
    # won't be added since defining the method how
    # treat the boundary condition.
    # line collision? dummy particles?
    return []
end

function GenBoundaries(inAnsDataStruct::AnalysisDataStruct)
    ## CREATE BOUNDARY LINES ##
    # Boundary line is a object which shape never
    # change, but only 회전/병진. at this time, it
    # won't be added since defining the method how
    # treat the boundary condition.
    # line collision? dummy particles?
    return []
end

# private

function FillInitialBox(inAnsDataStruct::AnalysisDataStruct, arParticleData::Vector{ParticleData})
    ID = 1
    for initBox_i in 1:length(inAnsDataStruct.a_initialBox)
        initBox_phaseID = inAnsDataStruct.a_initialBox[initBox_i].phaseID
        initBox_box     = inAnsDataStruct.a_initialBox[initBox_i].box
        initBox_vel     = inAnsDataStruct.a_initialBox[initBox_i].vel
        initBox_type    = inAnsDataStruct.a_phases[initBox_phaseID].type
        if initBox_type == "Fluid"
            initBox_name      = inAnsDataStruct.a_phases[initBox_phaseID].Fluid.name
            initBox_size      = inAnsDataStruct.a_phases[initBox_phaseID].Fluid.size
            initBox_density   = inAnsDataStruct.a_phases[initBox_phaseID].Fluid.density
            initBox_viscosity = inAnsDataStruct.a_phases[initBox_phaseID].Fluid.viscosity
            # initBox_mass      = initBox_density * (π*(initBox_size*0.5)^2.0) # Circle
            # initBox_mass      = initBox_density * (initBox_size^2.0) # Box
            initBox_mass      = initBox_density * ((initBox_size^2.0)*0.85 + (π*(initBox_size*0.5)^2.0)*0.15)
        end
        boxDX = initBox_box.endPoint.x - initBox_box.staPoint.x
        boxDY = initBox_box.endPoint.y - initBox_box.staPoint.y
        for ix in 1:div(boxDX+initBox_size*0.5,initBox_size)
            for iy in 1:div(boxDY+initBox_size*0.5,initBox_size)
                xx = initBox_box.staPoint.x + (ix-0.5)*initBox_size
                yy = initBox_box.staPoint.y + (iy-0.5)*initBox_size
                arParticleData[ID] = ParticleData(
                                        0,              # gridID :: Int64
                                        initBox_vel,    # vel    :: Vec2
                                        Vec2(xx,yy),    # pos    :: Vec2
                                        Vec2(xx,yy),    # temppos:: Vec2
                                        0.0,            # lambda :: Float64
                                        initBox_mass,   # mass   :: Float64
                                        initBox_phaseID)# phase  :: Int64
                ID += 1
            end
        end
    end
end


#----------------------------------------------------------------------------------------------#
#                                    Amount Type Interface                                     #
#----------------------------------------------------------------------------------------------#

"""
`function deco end`\n
Interface to return a unique decorative `Symbol` from a method's argument type.
"""
function deco end

"""
`function ppu end`\n
Interface to pretty-print units.
"""
function ppu end

# A 191113-212130 benchmark showed amt(x) is faster than x.amt:
#
# ```julia-repl
# julia> u1 = u(1.0f0 ± 0.1f0, MO)
# ū₃₂: (1 ± 0.1) kJ/kmol
#
# julia> typeof(u1)
# uAmt{Float32,MM,MO}
#
# julia> @benchmark u1.amt
# ✂ ✂ ✂   median time:      26.918 ns (0.00% GC)   ✂ ✂ ✂
#
# julia> @benchmark amt(u1)
# ✂ ✂ ✂   median time:      16.710 ns (0.00% GC)   ✂ ✂ ✂
#
# ```

"""
`function amt end`\n
Interface to get an `AMOUNTS`' `:amt` field in a type-stable manner.
"""
function amt end

export deco, ppu, amt

#----------------------------------------------------------------------------------------------#
#                                     Generic Amount Type                                      #
#----------------------------------------------------------------------------------------------#

import Base: cp, convert
import Unicode: normalize
import Base: +, -, *, /

"""
Generic Amount type factory.
"""
function mkGenAmt(TYPE::Symbol,         # Type name:            :_Amt
                  SUPT::Symbol,         # Supertype:            :GenerAmt
                  SYMB::AbstractString, # Printing symbol:      "?"
                  WHAT::AbstractString, # Description:          "generic amounts"
                  DELT::Bool=false,     # Whether a Δ quantity
                 )
    # Constants
    i, f = DELT ? (3, 4) : (1, 2)
    𝑠SY = normalize((DELT ? "Δ" : "") * string(SYMB))
    # Documentation
    hiStr = tyArchy(eval(SUPT))
    dcStr = """
`struct $TYPE{𝗽<:PREC,𝘅<:EXAC} <: $SUPT{𝗽,𝘅}`\n
Precision-, and Exactness- parametric $WHAT amounts based in arbitrary units.\n
`$TYPE{𝗽,𝘅}` parameters are:\n
- Precision `𝗽<:Union{Float16,Float32,Float64,BigFloat}`;\n
- Exactness `𝘅<:Union{EX,MM}`, i.e., either a single, precise value or an uncertainty-bearing
  measurement, respectively;\n
A `$TYPE` can be natively constructed from the following argument types:\n
- A plain, unitless float;\n
- A plain, unitless `Measurement`; hence, any `AbstractFloat`;\n
- A `Quantity{AbstractFloat}` with any units.\n
## Hierarchy\n
`$(TYPE) <: $(hiStr)`
    """
    # @eval block
    @eval begin
        # Concrete type definition
        struct $TYPE{𝗽,𝘅} <: $SUPT{𝗽,𝘅}
            amt::UATY{𝗽} where 𝗽<:PREC
            # Inner, non-converting, parameter-determining constructors
            $TYPE(x::$TYPE{𝗽,𝘅}) where {𝗽<:PREC,𝘅<:EXAC} = new{𝗽,𝘅}(amt(x))
            $TYPE(x::Union{𝗽,UETY{𝗽}}) where 𝗽<:PREC = new{𝗽,EX}(_qty(x))
            $TYPE(x::Union{PMTY{𝗽},UMTY{𝗽}}) where 𝗽<:PREC = new{𝗽,MM}(_qty(x))
            # Inner, non-converting, fully-specified constructors
            (::Type{$TYPE{𝗽,EX}})(x::𝗽) where 𝗽<:PREC = new{𝗽,EX}(_qty(x))
            (::Type{$TYPE{𝗽,EX}})(x::PMTY{𝗽}) where 𝗽<:PREC = new{𝗽,EX}(_qty(x.val))
            (::Type{$TYPE{𝗽,MM}})(x::𝗽) where 𝗽<:PREC = new{𝗽,MM}(_qty(measurement(x)))
            (::Type{$TYPE{𝗽,MM}})(x::PMTY{𝗽}) where 𝗽<:PREC = new{𝗽,MM}(_qty(x))
        end
        # Type documentation
        @doc $dcStr $TYPE
        # Precision-changing external constructors
        (::Type{$TYPE{𝘀}})(x::$TYPE{𝗽,EX}) where {𝘀<:PREC,𝗽<:PREC} = begin
            $TYPE(𝘀(amt(x).val) * unit(amt(x)))
        end
        (::Type{$TYPE{𝘀}})(x::$TYPE{𝗽,MM}) where {𝘀<:PREC,𝗽<:PREC} = begin
            $TYPE(Measurement{𝘀}(amt(x).val) * unit(amt(x)))
        end
        # Precision+Exactness-changing external constructors
        (::Type{$TYPE{𝘀,EX}})(x::$TYPE{𝗽,EX}) where {𝘀<:PREC,𝗽<:PREC} = begin
            $TYPE(𝘀(amt(x).val) * unit(amt(x)))
        end
        (::Type{$TYPE{𝘀,EX}})(x::$TYPE{𝗽,MM}) where {𝘀<:PREC,𝗽<:PREC} = begin
            $TYPE(𝘀(amt(x).val.val) * unit(amt(x)))
        end
        (::Type{$TYPE{𝘀,MM}})(x::$TYPE{𝗽,EX},
                              e::𝘀=𝘀(max(eps(𝘀), eps(amt(x).val)))) where {𝘀<:PREC,
                                                                           𝗽<:PREC} = begin
            $TYPE(measurement(𝘀(amt(x).val), e) * unit(amt(x)))
        end
        (::Type{$TYPE{𝘀,MM}})(x::$TYPE{𝗽,MM}) where {𝘀<:PREC,𝗽<:PREC} = begin
            $TYPE(Measurement{𝘀}(amt(x).val) * unit(amt(x)))
        end
        # Type export
        export $TYPE
        # Type-stabler wrapped amount obtaining function
        amt(x::$TYPE{𝗽,EX}) where 𝗽<:PREC = x.amt::Quantity{𝗽}
        amt(x::$TYPE{𝗽,MM}) where 𝗽<:PREC = x.amt::Quantity{Measurement{𝗽}}
        # Type-specific functions
        deco(x::$TYPE{𝗽,𝘅} where {𝗽,𝘅}) = Symbol($𝑠SY)
        ppu(x::$TYPE{𝗽,𝘅} where {𝗽,𝘅}) = string(unit(amt(x)))
        # Conversions
        convert(::Type{$TYPE{𝘀,𝘅}},
                y::$TYPE{𝗽,𝘅}) where {𝘀<:PREC,𝗽<:PREC,𝘅<:EXAC} = begin
            $TYPE{promote_type(𝘀,𝗽),𝘅}(y)
        end
        convert(::Type{$TYPE{𝘀,𝘆}},
                y::$TYPE{𝗽,𝘅}) where {𝘀<:PREC,𝗽<:PREC,𝘆<:EXAC,𝘅<:EXAC} = begin
            $TYPE{promote_type(𝘀,𝗽),promote_type(𝘆,𝘅)}(y)
        end
        # Promotion rules
        promote_rule(::Type{$TYPE{𝘀,𝘆}},
                     ::Type{$TYPE{𝗽,𝘅}}) where {𝘀<:PREC,𝗽<:PREC,𝘆<:EXAC,𝘅<:EXAC} = begin
            $TYPE{promote_type(𝘀,𝗽),promote_type(𝘆,𝘅)}
        end
        # same-type sum,sub with Unitful promotion
        +(x::$TYPE{𝘀,𝘆}, y::$TYPE{𝗽,𝘅}) where {𝘀<:PREC,𝗽<:PREC,𝘆<:EXAC,𝘅<:EXAC} = begin
            $TYPE(+(amt(x), amt(y)))
        end
        -(x::$TYPE{𝘀,𝘆}, y::$TYPE{𝗽,𝘅}) where {𝘀<:PREC,𝗽<:PREC,𝘆<:EXAC,𝘅<:EXAC} = begin
            $TYPE(-(amt(x), amt(y)))
        end
        # scalar mul,div with Unitful promotion
        *(y::plnF{𝘀}, x::$TYPE{𝗽}) where {𝘀<:PREC,𝗽<:PREC} = $TYPE(*(amt(x), y))
        *(x::$TYPE{𝗽}, y::plnF{𝘀}) where {𝘀<:PREC,𝗽<:PREC} = $TYPE(*(amt(x), y))
        /(x::$TYPE{𝗽}, y::plnF{𝘀}) where {𝘀<:PREC,𝗽<:PREC} = $TYPE(/(amt(x), y))
        # Type-preserving scalar mul,div
        *(y::REAL, x::$TYPE{𝗽}) where 𝗽<:PREC = $TYPE(*(amt(x), 𝗽(y)))
        *(x::$TYPE{𝗽}, y::REAL) where 𝗽<:PREC = $TYPE(*(amt(x), 𝗽(y)))
        /(x::$TYPE{𝗽}, y::REAL) where 𝗽<:PREC = $TYPE(/(amt(x), 𝗽(y)))
    end
end

#----------------------------------------------------------------------------------------------#
#                                 Generic Amount Declarations                                  #
#----------------------------------------------------------------------------------------------#

# The fallback generic amount
mkGenAmt(:_Amt  , :GenerAmt , "?"   , "generic amounts"     , false )

#----------------------------------------------------------------------------------------------#
#                                  Whole Amount Type Factory                                   #
#----------------------------------------------------------------------------------------------#

"""
Whole Amount type factory.
"""
function mkWhlAmt(TYPE::Symbol,         # Type name:            :sysT
                  SUPT::Symbol,         # Supertype:            :WProperty
                  FNAM::Symbol,         # Function Name:        :T
                  SYMB::AbstractString, # Printing symbol:      "T"
                  UNIT::Unitful.Units,  # SY quantity units:    u"K"
                  USTR::AbstractString, # PrettyPrinting units: "K"
                  WHAT::AbstractString, # Description:          "temperature"
                  DELT::Bool=false,     # Whether a Δ quantity
                 )
    # Constants
    uSY = UNIT
    𝑢SY = typeof(uSY)
    𝑑SY = dimension(uSY)
    i, f = DELT ? (3, 4) : (1, 2)
    𝑠SY = normalize((DELT ? "Δ" : "") * string(SYMB))
    # Documentation
    hiStr = tyArchy(eval(SUPT))
    dcStr = """
`struct $TYPE{𝗽<:PREC,𝘅<:EXAC} <: $SUPT{𝗽,𝘅}`\n
Precision-, and Exactness- parametric $WHAT amounts based in $USTR.\n
`$TYPE{𝗽,𝘅}` parameters are:\n
- Precision `𝗽<:Union{Float16,Float32,Float64,BigFloat}`;\n
- Exactness `𝘅<:Union{EX,MM}`, i.e., either a single, precise value or an uncertainty-bearing
  measurement, respectively;\n
A `$TYPE` can be natively constructed from the following argument types:\n
- A plain, unitless float;\n
- A plain, unitless `Measurement`; hence, any `AbstractFloat`;\n
- A `Quantity{AbstractFloat}` with compatible units.\n
Constructors determine all parameters from their arguments.\n
## Hierarchy\n
`$(TYPE) <: $(hiStr)`
    """
    # @eval block
    @eval begin
        # Concrete type definition
        struct $TYPE{𝗽,𝘅} <: $SUPT{𝗽,𝘅}
            amt::UATY{𝗽,$𝑑SY,$𝑢SY} where 𝗽<:PREC
            # Inner, non-converting, parameter-determining constructors
            # ---------------------------------------------------------
            # Copy constructor
            $TYPE(x::$TYPE{𝗽,𝘅}) where {𝗽<:PREC,𝘅<:EXAC} = new{𝗽,𝘅}(amt(x))
            # Plain constructors enforce default units & avoid unit conversion
            $TYPE(x::𝗽) where 𝗽<:PREC = new{𝗽,EX}(_qty(x * $uSY))
            $TYPE(x::PMTY{𝗽}) where 𝗽<:PREC = new{𝗽,MM}(_qty(x * $uSY))
            # Quantity constructors have to perform unit conversion despite matching dimensions
            $TYPE(x::UETY{𝗽,$𝑑SY}) where 𝗽<:PREC = new{𝗽,EX}(_qty(uconvert($uSY, x)))
            $TYPE(x::UMTY{𝗽,$𝑑SY}) where 𝗽<:PREC = new{𝗽,MM}(_qty(uconvert($uSY, x)))
            # Inner, non-converting, fully-specified constructors
            # ---------------------------------------------------
            (::Type{$TYPE{𝗽,EX}})(x::𝗽) where 𝗽<:PREC = new{𝗽,EX}(_qty(x * $uSY))
            (::Type{$TYPE{𝗽,EX}})(x::PMTY{𝗽}) where 𝗽<:PREC = new{𝗽,EX}(_qty(x.val * $uSY))
            (::Type{$TYPE{𝗽,MM}})(x::𝗽) where 𝗽<:PREC = new{𝗽,MM}(_qty(measurement(x) * $uSY))
            (::Type{$TYPE{𝗽,MM}})(x::PMTY{𝗽}) where 𝗽<:PREC = new{𝗽,MM}(_qty(x * $uSY))
        end
        # Type documentation
        @doc $dcStr $TYPE
        # Precision-changing external constructors
        (::Type{$TYPE{𝘀}})(x::$TYPE{𝗽,EX}
                          ) where {𝘀<:PREC,𝗽<:PREC} = $TYPE(𝘀(amt(x).val))
        (::Type{$TYPE{𝘀}})(x::$TYPE{𝗽,MM}
                          ) where {𝘀<:PREC,𝗽<:PREC} = $TYPE(Measurement{𝘀}(amt(x).val))
        # Precision+Exactness-changing external constructors
        (::Type{$TYPE{𝘀,EX}})(x::$TYPE{𝗽,EX}
                             ) where {𝘀<:PREC,𝗽<:PREC} = $TYPE(𝘀(amt(x).val))
        (::Type{$TYPE{𝘀,EX}})(x::$TYPE{𝗽,MM}
                             ) where {𝘀<:PREC,𝗽<:PREC} = $TYPE(𝘀(amt(x).val.val))
        (::Type{$TYPE{𝘀,MM}})(x::$TYPE{𝗽,EX},
                              e::𝘀=𝘀(max(eps(𝘀),eps(amt(x).val)))
                             ) where {𝘀<:PREC,𝗽<:PREC} = $TYPE(measurement(𝘀(amt(x).val), e))
        (::Type{$TYPE{𝘀,MM}})(x::$TYPE{𝗽,MM}
                             ) where {𝘀<:PREC,𝗽<:PREC} = $TYPE(Measurement{𝘀}(amt(x).val))
        # Type export
        export $TYPE
        # Type-stable wrapped amount obtaining function
        amt(x::$TYPE{𝗽,EX}) where 𝗽<:PREC = x.amt::Quantity{𝗽,$𝑑SY,$𝑢SY}
        amt(x::$TYPE{𝗽,MM}) where 𝗽<:PREC = x.amt::Quantity{Measurement{𝗽},$𝑑SY,$𝑢SY}
        # Type-specific functions
        deco(x::$TYPE{𝗽,𝘅} where {𝗽,𝘅}) = Symbol($𝑠SY)
        ppu(x::$TYPE{𝗽,𝘅} where {𝗽,𝘅}) = $USTR
        # Indirect construction from plain
        $FNAM(x::plnF) = $TYPE(x)
        $FNAM(x::REAL) = $TYPE(float(x))
        # Indirect construction from quantity
        $FNAM(x::UATY{𝗽,$𝑑SY}) where 𝗽<:PREC = $TYPE(x)
        $FNAM(x::uniR{𝗽,$𝑑SY}) where 𝗽<:REAL = $TYPE(float(x.val) * unit(x))
        export $FNAM
        # Conversions
        convert(::Type{$TYPE{𝘀,𝘅}},
                y::$TYPE{𝗽,𝘅}) where {𝘀<:PREC,𝗽<:PREC,𝘅<:EXAC} = begin
            $TYPE{promote_type(𝘀,𝗽),𝘅}(y)
        end
        convert(::Type{$TYPE{𝘀,𝘆}},
                y::$TYPE{𝗽,𝘅}) where {𝘀<:PREC,𝗽<:PREC,𝘆<:EXAC,𝘅<:EXAC} = begin
            $TYPE{promote_type(𝘀,𝗽),promote_type(𝘆,𝘅)}(y)
        end
        # Promotion rules
        promote_rule(::Type{$TYPE{𝘀,𝘆}},
                     ::Type{$TYPE{𝗽,𝘅}}) where {𝘀<:PREC,𝗽<:PREC,𝘆<:EXAC,𝘅<:EXAC} = begin
            $TYPE{promote_type(𝘀,𝗽),promote_type(𝘆,𝘅)}
        end
        # same-type sum,sub with Unitful promotion
        +(x::$TYPE{𝘀,𝘆}, y::$TYPE{𝗽,𝘅}) where {𝘀<:PREC,𝗽<:PREC,𝘆<:EXAC,𝘅<:EXAC} = begin
            $TYPE(+(amt(x), amt(y)))
        end
        -(x::$TYPE{𝘀,𝘆}, y::$TYPE{𝗽,𝘅}) where {𝘀<:PREC,𝗽<:PREC,𝘆<:EXAC,𝘅<:EXAC} = begin
            $TYPE(-(amt(x), amt(y)))
        end
        # scalar mul,div with Unitful promotion
        *(y::plnF{𝘀}, x::$TYPE{𝗽}) where {𝘀<:PREC,𝗽<:PREC} = $TYPE(*(amt(x), y))
        *(x::$TYPE{𝗽}, y::plnF{𝘀}) where {𝘀<:PREC,𝗽<:PREC} = $TYPE(*(amt(x), y))
        /(x::$TYPE{𝗽}, y::plnF{𝘀}) where {𝘀<:PREC,𝗽<:PREC} = $TYPE(/(amt(x), y))
        # Type-preserving scalar mul,div
        *(y::REAL, x::$TYPE{𝗽}) where 𝗽<:PREC = $TYPE(*(amt(x), 𝗽(y)))
        *(x::$TYPE{𝗽}, y::REAL) where 𝗽<:PREC = $TYPE(*(amt(x), 𝗽(y)))
        /(x::$TYPE{𝗽}, y::REAL) where 𝗽<:PREC = $TYPE(/(amt(x), 𝗽(y)))
    end
end

#----------------------------------------------------------------------------------------------#
#                           Thermodynamic Whole Amount Declarations                            #
#----------------------------------------------------------------------------------------------#

# Regular properties -- \bb#<TAB> velocity/speed function names
mkWhlAmt(:sysT  , :WProperty, :T    , "T"   , u"K"          , "K"       , "temperature"         , false )
mkWhlAmt(:sysP  , :WProperty, :P    , "P"   , u"kPa"        , "kPa"     , "pressure"            , false )
mkWhlAmt(:VELO  , :WProperty, :velo , "𝕍"   , u"√(kJ/kg)"   , "√kJ/kg"  , "velocity"            , false )
mkWhlAmt(:SPEE  , :WProperty, :spee , "𝕧"   , u"m/s"        , "m/s"     , "speed"               , false )

# Regular unranked -- \sans#<TAB> function names
mkWhlAmt(:TIME  , :WUnranked, :TIME , "𝗍"   , u"s"          , "s"       , "time"                , false )
mkWhlAmt(:grav  , :WUnranked, :grav , "𝗀"   , u"m/s^2"      , "m/s²"    , "gravity"             , false )
mkWhlAmt(:alti  , :WUnranked, :alti , "𝗓"   , u"m"          , "m"       , "altitude"            , false )

#----------------------------------------------------------------------------------------------#
#                                  Based Amount Type Factory                                   #
#----------------------------------------------------------------------------------------------#

"""
Based Amount type factory.
"""
function mkBasAmt(TYPE::Symbol,         # Type Name:            :uAmt
                  SUPT::Symbol,         # Supertype:            :BProperty
                  FNAM::Symbol,         # Function Name:        :u
                  SYMB::AbstractString, # Printing symbol:      "U"
                  UNIT::Unitful.Units,  # SY quantity units:    u"kJ"
                  USTR::AbstractString, # PrettyPrinting units: "K"
                  WHAT::AbstractString, # Description:          "internal energy"
                  DELT::Bool=false;     # Whether a Δ quantity
                  bsym::NTuple{4,Symbol}=(:none,:none,:none,:none)
                 )
    # Constants
    uSY = UNIT
    uDT = UNIT / u"s"
    uMA = UNIT / u"kg"
    uMO = UNIT / u"kmol"
    𝑢SY = typeof(uSY)
    𝑢DT = typeof(uDT)
    𝑢MA = typeof(uMA)
    𝑢MO = typeof(uMO)
    𝑑SY = dimension(uSY)
    𝑑DT = dimension(uDT)
    𝑑MA = dimension(uMA)
    𝑑MO = dimension(uMO)
    i, f = DELT ? (3, 4) : (1, 2)
    𝑠SY = bsym[1] == :none ?
        normalize((DELT ? "Δ" : "") * uppercase(string(SYMB))) :
        string(bsym[1])
    𝑠DT = bsym[2] == :none ?
        normalize(string(𝑠SY[1:i], "\u0307", 𝑠SY[f:end])) :
        string(bsym[2])
    𝑠MA = bsym[3] == :none ?
        normalize((DELT ? "Δ" : "") * lowercase(string(SYMB))) :
        string(bsym[3])
    𝑠MO = bsym[4] == :none ?
        normalize(string(𝑠MA[1:i], "\u0304", 𝑠MA[f:end])) :
        string(bsym[4])
    # Documentation
    hiStr = tyArchy(eval(SUPT))
    dcStr = """
`struct $TYPE{𝗽<:PREC,𝘅<:EXAC,𝗯<:BASE} <: $SUPT{𝗽,𝘅,𝗯}`\n
Precision-, Exactness-, and Base- parametric $WHAT amounts based in $USTR.\n
`$TYPE{𝗽,𝘅,𝗯}` parameters are:\n
- Precision `𝗽<:Union{Float16,Float32,Float64,BigFloat}`;\n
- Exactness `𝘅<:Union{EX,MM}`, i.e., either a single, precise value or an uncertainty-bearing
  measurement, respectively;\n
- Thermodynamic base `𝗯<:Union{SY,DT,MA,MO}` respectively for system, rate, mass, or molar
  quantities, respectively in units of $(uSY), $(uDT), $(uMA), or $(uMO).\n
A `$TYPE` can be natively constructed from the following argument types:\n
- A plain, unitless float;\n
- A plain, unitless `Measurement`; hence, any `AbstractFloat`;\n
- A `Quantity{AbstractFloat}` with compatible units.\n
Constructors determine parameters from their arguments. `Quantity` constructors do not need a
base argument. Plain, `AbstractFloat` ones require the base argument.\n
## Hierarchy\n
`$(TYPE) <: $(hiStr)`
    """
    # @eval block
    @eval begin
        # Concrete type definition
        struct $TYPE{𝗽,𝘅,𝗯} <: $SUPT{𝗽,𝘅,𝗯}
            amt::Union{UATY{𝗽,$𝑑SY,$𝑢SY},UATY{𝗽,$𝑑DT,$𝑢DT},
                       UATY{𝗽,$𝑑MA,$𝑢MA},UATY{𝗽,$𝑑MO,$𝑢MO}} where 𝗽<:PREC
            # Inner, non-converting, parameter-determining constructors
            # ---------------------------------------------------------
            # Copy constructor
            $TYPE(x::$TYPE{𝗽,𝘅,𝗯}) where {𝗽<:PREC,𝘅<:EXAC,𝗯<:BASE} = new{𝗽,𝘅,𝗯}(amt(x))
            # Plain constructors enforce default units & avoid unit conversion
            # Plain Exact (𝗽<:PREC) float constructors
            $TYPE(x::𝗽, ::Type{SY}) where 𝗽<:PREC = new{𝗽,EX,SY}(_qty(x * $uSY))
            $TYPE(x::𝗽, ::Type{DT}) where 𝗽<:PREC = new{𝗽,EX,DT}(_qty(x * $uDT))
            $TYPE(x::𝗽, ::Type{MA}) where 𝗽<:PREC = new{𝗽,EX,MA}(_qty(x * $uMA))
            $TYPE(x::𝗽, ::Type{MO}) where 𝗽<:PREC = new{𝗽,EX,MO}(_qty(x * $uMO))
            # Plain Measurement (PMTY) constructors
            $TYPE(x::PMTY{𝗽}, ::Type{SY}) where 𝗽<:PREC = new{𝗽,MM,SY}(_qty(x * $uSY))
            $TYPE(x::PMTY{𝗽}, ::Type{DT}) where 𝗽<:PREC = new{𝗽,MM,DT}(_qty(x * $uDT))
            $TYPE(x::PMTY{𝗽}, ::Type{MA}) where 𝗽<:PREC = new{𝗽,MM,MA}(_qty(x * $uMA))
            $TYPE(x::PMTY{𝗽}, ::Type{MO}) where 𝗽<:PREC = new{𝗽,MM,MO}(_qty(x * $uMO))
            # Quantity constructors have to perform unit conversion despite matching dimensions
            # United Exact (UETY) constructors
            $TYPE(x::UETY{𝗽,$𝑑SY}) where 𝗽<:PREC = new{𝗽,EX,SY}(_qty(uconvert($uSY, x)))
            $TYPE(x::UETY{𝗽,$𝑑DT}) where 𝗽<:PREC = new{𝗽,EX,DT}(_qty(uconvert($uDT, x)))
            $TYPE(x::UETY{𝗽,$𝑑MA}) where 𝗽<:PREC = new{𝗽,EX,MA}(_qty(uconvert($uMA, x)))
            $TYPE(x::UETY{𝗽,$𝑑MO}) where 𝗽<:PREC = new{𝗽,EX,MO}(_qty(uconvert($uMO, x)))
            # United Measurement (UMTY) constructors
            $TYPE(x::UMTY{𝗽,$𝑑SY}) where 𝗽<:PREC = new{𝗽,MM,SY}(_qty(uconvert($uSY, x)))
            $TYPE(x::UMTY{𝗽,$𝑑DT}) where 𝗽<:PREC = new{𝗽,MM,DT}(_qty(uconvert($uDT, x)))
            $TYPE(x::UMTY{𝗽,$𝑑MA}) where 𝗽<:PREC = new{𝗽,MM,MA}(_qty(uconvert($uMA, x)))
            $TYPE(x::UMTY{𝗽,$𝑑MO}) where 𝗽<:PREC = new{𝗽,MM,MO}(_qty(uconvert($uMO, x)))
            # Inner, non-converting, fully-specified constructors
            # ---------------------------------------------------
            # SY-based constructors
            (::Type{$TYPE{𝗽,EX,SY}})(x::𝗽) where 𝗽<:PREC = begin
                new{𝗽,EX,SY}(_qty(             x * $uSY))
            end
            (::Type{$TYPE{𝗽,EX,SY}})(x::PMTY{𝗽}) where 𝗽<:PREC = begin
                new{𝗽,EX,SY}(_qty(         x.val * $uSY))
            end
            (::Type{$TYPE{𝗽,MM,SY}})(x::𝗽) where 𝗽<:PREC = begin
                new{𝗽,MM,SY}(_qty(measurement(x) * $uSY))
            end
            (::Type{$TYPE{𝗽,MM,SY}})(x::PMTY{𝗽}) where 𝗽<:PREC = begin
                new{𝗽,MM,SY}(_qty(             x * $uSY))
            end
            # DT-based constructors
            (::Type{$TYPE{𝗽,EX,DT}})(x::𝗽) where 𝗽<:PREC = begin
                new{𝗽,EX,DT}(_qty(             x * $uDT))
            end
            (::Type{$TYPE{𝗽,EX,DT}})(x::PMTY{𝗽}) where 𝗽<:PREC = begin
                new{𝗽,EX,DT}(_qty(         x.val * $uDT))
            end
            (::Type{$TYPE{𝗽,MM,DT}})(x::𝗽) where 𝗽<:PREC = begin
                new{𝗽,MM,DT}(_qty(measurement(x) * $uDT))
            end
            (::Type{$TYPE{𝗽,MM,DT}})(x::PMTY{𝗽}) where 𝗽<:PREC = begin
                new{𝗽,MM,DT}(_qty(             x * $uDT))
            end
            # MA-based constructors
            (::Type{$TYPE{𝗽,EX,MA}})(x::𝗽) where 𝗽<:PREC = begin
                new{𝗽,EX,MA}(_qty(             x * $uMA))
            end
            (::Type{$TYPE{𝗽,EX,MA}})(x::PMTY{𝗽}) where 𝗽<:PREC = begin
                new{𝗽,EX,MA}(_qty(         x.val * $uMA))
            end
            (::Type{$TYPE{𝗽,MM,MA}})(x::𝗽) where 𝗽<:PREC = begin
                new{𝗽,MM,MA}(_qty(measurement(x) * $uMA))
            end
            (::Type{$TYPE{𝗽,MM,MA}})(x::PMTY{𝗽}) where 𝗽<:PREC = begin
                new{𝗽,MM,MA}(_qty(             x * $uMA))
            end
            # MO-based constructors
            (::Type{$TYPE{𝗽,EX,MO}})(x::𝗽) where 𝗽<:PREC = begin
                new{𝗽,EX,MO}(_qty(             x * $uMO))
            end
            (::Type{$TYPE{𝗽,EX,MO}})(x::PMTY{𝗽}) where 𝗽<:PREC = begin
                new{𝗽,EX,MO}(_qty(         x.val * $uMO))
            end
            (::Type{$TYPE{𝗽,MM,MO}})(x::𝗽) where 𝗽<:PREC = begin
                new{𝗽,MM,MO}(_qty(measurement(x) * $uMO))
            end
            (::Type{$TYPE{𝗽,MM,MO}})(x::PMTY{𝗽}) where 𝗽<:PREC = begin
                new{𝗽,MM,MO}(_qty(             x * $uMO))
            end
        end
        # Type documentation
        @doc $dcStr $TYPE
        # Precision-changing external constructors
        (::Type{$TYPE{𝘀}})(x::$TYPE{𝗽,EX,𝗯}) where {𝘀<:PREC,𝗽<:PREC,𝗯<:BASE} = begin
            $TYPE(𝘀(amt(x).val), 𝗯)
        end
        (::Type{$TYPE{𝘀}})(x::$TYPE{𝗽,MM,𝗯}) where {𝘀<:PREC,𝗽<:PREC,𝗯<:BASE} = begin
            $TYPE(Measurement{𝘀}(amt(x).val), 𝗯)
        end
        # Precision+Exactness-changing external constructors
        (::Type{$TYPE{𝘀,EX}})(x::$TYPE{𝗽,EX,𝗯}) where {𝘀<:PREC,𝗽<:PREC,𝗯<:BASE} = begin
            $TYPE(𝘀(amt(x).val), 𝗯)
        end
        (::Type{$TYPE{𝘀,EX}})(x::$TYPE{𝗽,MM,𝗯}) where {𝘀<:PREC,𝗽<:PREC,𝗯<:BASE} = begin
            $TYPE(𝘀(amt(x).val.val), 𝗯)
        end
        (::Type{$TYPE{𝘀,MM}})(x::$TYPE{𝗽,EX,𝗯},
                            e::𝘀=𝘀(max(eps(𝘀),eps(amt(x).val)))
                            ) where {𝘀<:PREC,𝗽<:PREC,𝗯<:BASE} = begin
            $TYPE(measurement(𝘀(amt(x).val), e), 𝗯)
        end
        (::Type{$TYPE{𝘀,MM}})(x::$TYPE{𝗽,MM,𝗯}) where {𝘀<:PREC,𝗽<:PREC,𝗯<:BASE} = begin
            $TYPE(Measurement{𝘀}(amt(x).val), 𝗯)
        end
        # Type export
        export $TYPE
        # Type-stable wrapped amount obtaining function
        amt(x::$TYPE{𝗽,EX,SY}) where 𝗽<:PREC = x.amt::Quantity{𝗽,$𝑑SY,$𝑢SY}
        amt(x::$TYPE{𝗽,EX,DT}) where 𝗽<:PREC = x.amt::Quantity{𝗽,$𝑑DT,$𝑢DT}
        amt(x::$TYPE{𝗽,EX,MA}) where 𝗽<:PREC = x.amt::Quantity{𝗽,$𝑑MA,$𝑢MA}
        amt(x::$TYPE{𝗽,EX,MO}) where 𝗽<:PREC = x.amt::Quantity{𝗽,$𝑑MO,$𝑢MO}
        amt(x::$TYPE{𝗽,MM,SY}) where 𝗽<:PREC = x.amt::Quantity{Measurement{𝗽},$𝑑SY,$𝑢SY}
        amt(x::$TYPE{𝗽,MM,DT}) where 𝗽<:PREC = x.amt::Quantity{Measurement{𝗽},$𝑑DT,$𝑢DT}
        amt(x::$TYPE{𝗽,MM,MA}) where 𝗽<:PREC = x.amt::Quantity{Measurement{𝗽},$𝑑MA,$𝑢MA}
        amt(x::$TYPE{𝗽,MM,MO}) where 𝗽<:PREC = x.amt::Quantity{Measurement{𝗽},$𝑑MO,$𝑢MO}
        # Type-specific functions
        deco(x::$TYPE{𝗽,𝘅,SY} where {𝗽,𝘅}) = Symbol($𝑠SY)
        deco(x::$TYPE{𝗽,𝘅,DT} where {𝗽,𝘅}) = Symbol($𝑠DT)
        deco(x::$TYPE{𝗽,𝘅,MA} where {𝗽,𝘅}) = Symbol($𝑠MA)
        deco(x::$TYPE{𝗽,𝘅,MO} where {𝗽,𝘅}) = Symbol($𝑠MO)
        ppu(x::$TYPE{𝗽,𝘅,SY} where {𝗽,𝘅}) = $USTR
        ppu(x::$TYPE{𝗽,𝘅,DT} where {𝗽,𝘅}) = $USTR * "/s"
        ppu(x::$TYPE{𝗽,𝘅,MA} where {𝗽,𝘅}) = $USTR * "/kg"
        ppu(x::$TYPE{𝗽,𝘅,MO} where {𝗽,𝘅}) = $USTR * "/kmol"
        # Indirect construction from plain
        $FNAM(x::plnF, b::Type{𝗯}=DEF[:IB]) where 𝗯<:BASE = $TYPE(x, b)
        $FNAM(x::REAL, b::Type{𝗯}=DEF[:IB]) where 𝗯<:BASE = $TYPE(float(x), b)
        # Indirect construction from quantity
        $FNAM(x::Union{UATY{𝗽,$𝑑SY},UATY{𝗽,$𝑑DT},
                       UATY{𝗽,$𝑑MA},UATY{𝗽,$𝑑MO}}) where 𝗽<:PREC = begin
            $TYPE(x)
        end
        $FNAM(x::Union{uniR{𝗽,$𝑑SY},uniR{𝗽,$𝑑DT},
                       uniR{𝗽,$𝑑MA},uniR{𝗽,$𝑑MO}}) where 𝗽<:REAL = begin
            $TYPE(float(x.val) * unit(x))
        end
        export $FNAM
        # Conversions - Change of base is _not_ a conversion
        # Same {EXAC,BASE}, {PREC}- conversion
        convert(::Type{$TYPE{𝘀,𝘅,𝗯}},
                y::$TYPE{𝗽,𝘅,𝗯}) where {𝘀<:PREC,𝗽<:PREC,𝘅<:EXAC,𝗯<:BASE} = begin
            $TYPE{promote_type(𝘀,𝗽),𝘅}(y)
        end
        # Same {BASE}, {PREC,EXAC}- conversion
        convert(::Type{$TYPE{𝘀,𝘆,𝗯}},
                y::$TYPE{𝗽,𝘅,𝗯}) where {𝘀<:PREC,𝗽<:PREC,𝘆<:EXAC,𝘅<:EXAC,𝗯<:BASE} = begin
            $TYPE{promote_type(𝘀,𝗽),promote_type(𝘆,𝘅)}(y)
        end
        # Promotion rules
        promote_rule(::Type{$TYPE{𝘀,𝘆,𝗯}},
                     ::Type{$TYPE{𝗽,𝘅,𝗯}}) where {𝘀<:PREC,𝗽<:PREC,
                                                  𝘆<:EXAC,𝘅<:EXAC,𝗯<:BASE} = begin
            $TYPE{promote_type(𝘀,𝗽),promote_type(𝘆,𝘅),𝗯}
        end
        # same-type sum,sub with Unitful promotion
        +(x::$TYPE{𝘀,𝘆,𝗯}, y::$TYPE{𝗽,𝘅,𝗯}) where {𝘀<:PREC,𝗽<:PREC,
                                                   𝘆<:EXAC,𝘅<:EXAC,
                                                   𝗯<:BASE} = begin
            $TYPE(+(amt(x), amt(y)))
        end
        -(x::$TYPE{𝘀,𝘆,𝗯}, y::$TYPE{𝗽,𝘅,𝗯}) where {𝘀<:PREC,𝗽<:PREC,
                                                   𝘆<:EXAC,𝘅<:EXAC,
                                                   𝗯<:BASE} = begin
            $TYPE(-(amt(x), amt(y)))
        end
        # scalar mul,div with Unitful promotion
        *(y::plnF{𝘀}, x::$TYPE{𝗽}) where {𝘀<:PREC,𝗽<:PREC} = $TYPE(*(amt(x), y))
        *(x::$TYPE{𝗽}, y::plnF{𝘀}) where {𝘀<:PREC,𝗽<:PREC} = $TYPE(*(amt(x), y))
        /(x::$TYPE{𝗽}, y::plnF{𝘀}) where {𝘀<:PREC,𝗽<:PREC} = $TYPE(/(amt(x), y))
        # Type-preserving scalar mul,div
        *(y::REAL, x::$TYPE{𝗽}) where 𝗽<:PREC = $TYPE(*(amt(x), 𝗽(y)))
        *(x::$TYPE{𝗽}, y::REAL) where 𝗽<:PREC = $TYPE(*(amt(x), 𝗽(y)))
        /(x::$TYPE{𝗽}, y::REAL) where 𝗽<:PREC = $TYPE(/(amt(x), 𝗽(y)))
    end
end

#----------------------------------------------------------------------------------------------#
#                           Thermodynamic Amount Group Declarations                            #
#----------------------------------------------------------------------------------------------#

# Mass / Mass fraction anomalous
mkBasAmt(:mAmt  , :BProperty, :m    , "m"   , u"kg"         , "kg"      , "mass"                , false , bsym=(:m , :ṁ , :mf, :M))
# Chemical amount / Molar fraction anomalous
mkBasAmt(:nAmt  , :BProperty, :N    , "N"   , u"kmol"       , "kmol"    , "chemical amount"     , false , bsym=(:N , :Ṅ , :n , :y))
# Gas constant / System constant anomalous
mkBasAmt(:RAmt  , :BProperty, :R    , "mR"  , u"kJ/K"       , "kJ/K"    , "gas constant"        , false , bsym=(:mR, :ṁR, :R , :R̄))
# Plank function anomalous
mkBasAmt(:rAmt  , :BProperty, :r    , "mr"  , u"kJ/K"       , "kJ/K"    , "Planck function"     , false , bsym=(:mr, :ṁr, :r , :r̄))

# Regular properties
mkBasAmt(:vAmt  , :BProperty, :v    , "V"   , u"m^3"        , "m³"      , "volume"              , false )
mkBasAmt(:uAmt  , :BProperty, :u    , "U"   , u"kJ"         , "kJ"      , "internal energy"     , false )
mkBasAmt(:hAmt  , :BProperty, :h    , "H"   , u"kJ"         , "kJ"      , "enthalpy"            , false )
mkBasAmt(:gAmt  , :BProperty, :g    , "G"   , u"kJ"         , "kJ"      , "Gibbs energy"        , false )
mkBasAmt(:aAmt  , :BProperty, :a    , "A"   , u"kJ"         , "kJ"      , "Helmholtz energy"    , false )
mkBasAmt(:eAmt  , :BProperty, :e    , "E"   , u"kJ"         , "kJ"      , "total energy"        , false )
mkBasAmt(:ekAmt , :BProperty, :ek   , "Ek"  , u"kJ"         , "kJ"      , "kinetic energy"      , false )
mkBasAmt(:epAmt , :BProperty, :ep   , "Ep"  , u"kJ"         , "kJ"      , "potential energy"    , false )
mkBasAmt(:sAmt  , :BProperty, :s    , "S"   , u"kJ/K"       , "kJ/K"    , "entropy"             , false )
mkBasAmt(:cpAmt , :BProperty, :cp   , "Cp"  , u"kJ/K"       , "kJ/K"    , "iso-P specific heat" , false )
mkBasAmt(:cvAmt , :BProperty, :cv   , "Cv"  , u"kJ/K"       , "kJ/K"    , "iso-v specific heat" , false )
mkBasAmt(:jAmt  , :BProperty, :j    , "J"   , u"kJ/K"       , "kJ/K"    , "Massieu function"    , false )

# Regular interactions
mkBasAmt(:qAmt  , :BInteract, :q    , "Q"   , u"kJ"         , "kJ"      , "heat"                , false )
mkBasAmt(:wAmt  , :BInteract, :w    , "W"   , u"kJ"         , "kJ"      , "work"                , false )
mkBasAmt(:ΔeAmt , :BInteract, :Δe   , "E"   , u"kJ"         , "kJ"      , "energy variation"    , true  )
mkBasAmt(:ΔsAmt , :BInteract, :Δs   , "S"   , u"kJ/K"       , "kJ/K"    , "entropy variation"   , true  )

#----------------------------------------------------------------------------------------------#
#                                      AMOUNT Type Unions                                      #
#----------------------------------------------------------------------------------------------#

# Unions of amounts of like units and thermodynamic classification, for same-unit operations

# --- energy
"""
`ENERGYP{𝗽,𝘅,𝗯} where {𝗽<:PREC,𝘅<:EXAC,𝗯<:BASE}`\n
Energy property type union.
"""
ENERGYP{𝗽,𝘅,𝗯} = Union{uAmt{𝗽,𝘅,𝗯},hAmt{𝗽,𝘅,𝗯},
                       gAmt{𝗽,𝘅,𝗯},aAmt{𝗽,𝘅,𝗯},
                       eAmt{𝗽,𝘅,𝗯},ekAmt{𝗽,𝘅,𝗯},
                       epAmt{𝗽,𝘅,𝗯}} where {𝗽<:PREC,𝘅<:EXAC,𝗯<:BASE}

"""
`ENERGYI{𝗽,𝘅,𝗯} where {𝗽<:PREC,𝘅<:EXAC,𝗯<:BASE}`\n
Energy interaction type union.
"""
ENERGYI{𝗽,𝘅,𝗯} = Union{qAmt{𝗽,𝘅,𝗯},wAmt{𝗽,𝘅,𝗯},
                       ΔeAmt{𝗽,𝘅,𝗯}} where {𝗽<:PREC,𝘅<:EXAC,𝗯<:BASE}

"""
`ENERGYA{𝗽,𝘅,𝗯} where {𝗽<:PREC,𝘅<:EXAC,𝗯<:BASE}`\n
Energy amount type union.
"""
ENERGYA{𝗽,𝘅,𝗯} = Union{ENERGYP{𝗽,𝘅,𝗯},ENERGYI{𝗽,𝘅,𝗯}} where {𝗽<:PREC,𝘅<:EXAC,𝗯<:BASE}

# --- entropy
"""
`NTROPYP{𝗽,𝘅,𝗯} where {𝗽<:PREC,𝘅<:EXAC,𝗯<:BASE}`\n
Entropy property type union.
"""
NTROPYP{𝗽,𝘅,𝗯} = Union{RAmt{𝗽,𝘅,𝗯},rAmt{𝗽,𝘅,𝗯},
                       sAmt{𝗽,𝘅,𝗯},jAmt{𝗽,𝘅,𝗯},
                       cpAmt{𝗽,𝘅,𝗯},cvAmt{𝗽,𝘅,𝗯}} where {𝗽<:PREC,𝘅<:EXAC,𝗯<:BASE}

"""
`NTROPYI{𝗽,𝘅,𝗯} where {𝗽<:PREC,𝘅<:EXAC,𝗯<:BASE}`\n
Entropy interaction type union.
"""
NTROPYI{𝗽,𝘅,𝗯} = Union{ΔsAmt{𝗽,𝘅,𝗯}} where {𝗽<:PREC,𝘅<:EXAC,𝗯<:BASE}

"""
`NTROPYA{𝗽,𝘅,𝗯} where {𝗽<:PREC,𝘅<:EXAC,𝗯<:BASE}`\n
Entropy amount type union.
"""
NTROPYA{𝗽,𝘅,𝗯} = Union{NTROPYP{𝗽,𝘅,𝗯},NTROPYI{𝗽,𝘅,𝗯}} where {𝗽<:PREC,𝘅<:EXAC,𝗯<:BASE}

# --- velocity
"""
`VELOCYP{𝗽,𝘅} where {𝗽<:PREC,𝘅<:EXAC}`\n
Velocity property type union.
"""
VELOCYP{𝗽,𝘅,𝗯} = Union{VELO{𝗽,𝘅},SPEE{𝗽,𝘅}} where {𝗽<:PREC,𝘅<:EXAC}

#----------------------------------------------------------------------------------------------#
#                                       Pretty Printing                                        #
#----------------------------------------------------------------------------------------------#

import Base: show
import Formatting: sprintf1

# Auxiliar method
function subscript(x::Int)
    asSub(c::Char) = Char(Int(c) - Int('0') + Int('₀'))
    map(asSub, "$(x)")
end

# Precision decoration
pDeco(::Type{Float16})  = DEF[:showPrec] ? subscript(16) : ""
pDeco(::Type{Float32})  = DEF[:showPrec] ? subscript(32) : ""
pDeco(::Type{Float64})  = DEF[:showPrec] ? subscript(64) : ""
pDeco(::Type{BigFloat}) = DEF[:showPrec] ? subscript(precision(BigFloat)) : ""

# Custom printing
Base.show(io::IO, x::AMOUNTS{𝗽,EX}) where 𝗽<:PREC = begin
    if DEF[:pprint]
        print(io,
            "$(string(deco(x)))$(pDeco(𝗽)): ",
            sprintf1("%.$(DEF[:showSigD])g", amt(x).val),
            " ", ppu(x))
    else
        Base.show_default(io, x)
    end
end

Base.show(io::IO, x::AMOUNTS{𝗽,MM}) where 𝗽<:PREC = begin
    if DEF[:pprint]
        print(io,
            "$(string(deco(x)))$(pDeco(𝗽)): (",
            sprintf1("%.$(DEF[:showSigD])g", amt(x).val.val),
            " ± ",
            sprintf1("%.2g", amt(x).val.err),
            ") ", ppu(x))
    else
        Base.show_default(io, x)
    end
end


function create_cache_analysis(analyzer, mesh::TreeMesh{3},
                               equations, dg::DG, cache,
                               RealT, uEltype)

  # pre-allocate buffers
  # We use `StrideArray`s here since these buffers are used in performance-critical
  # places and the additional information passed to the compiler makes them faster
  # than native `Array`s.
  u_local = StrideArray(undef, uEltype,
                        StaticInt(nvariables(equations)), StaticInt(nnodes(analyzer)), StaticInt(nnodes(analyzer)), StaticInt(nnodes(analyzer)))
  u_tmp1  = StrideArray(undef, uEltype,
                        StaticInt(nvariables(equations)), StaticInt(nnodes(analyzer)), StaticInt(nnodes(dg)), StaticInt(nnodes(dg)))
  u_tmp2  = StrideArray(undef, uEltype,
                        StaticInt(nvariables(equations)), StaticInt(nnodes(analyzer)), StaticInt(nnodes(analyzer)), StaticInt(nnodes(dg)))
  x_local = StrideArray(undef, RealT,
                        StaticInt(ndims(equations)), StaticInt(nnodes(analyzer)), StaticInt(nnodes(analyzer)), StaticInt(nnodes(analyzer)))
  x_tmp1  = StrideArray(undef, RealT,
                        StaticInt(ndims(equations)), StaticInt(nnodes(analyzer)), StaticInt(nnodes(dg)), StaticInt(nnodes(dg)))
  x_tmp2  = StrideArray(undef, RealT,
                        StaticInt(ndims(equations)), StaticInt(nnodes(analyzer)), StaticInt(nnodes(analyzer)), StaticInt(nnodes(dg)))

  return (; u_local, u_tmp1, u_tmp2, x_local, x_tmp1, x_tmp2)
end

function create_cache_analysis(analyzer, mesh::CurvedMesh{3},
                               equations, dg::DG, cache,
                               RealT, uEltype)

  # pre-allocate buffers
  # We use `StrideArray`s here since these buffers are used in performance-critical
  # places and the additional information passed to the compiler makes them faster
  # than native `Array`s.
  u_local = StrideArray(undef, uEltype,
                        StaticInt(nvariables(equations)), StaticInt(nnodes(analyzer)), StaticInt(nnodes(analyzer)), StaticInt(nnodes(analyzer)))
  u_tmp1  = StrideArray(undef, uEltype,
                        StaticInt(nvariables(equations)), StaticInt(nnodes(analyzer)), StaticInt(nnodes(dg)), StaticInt(nnodes(dg)))
  u_tmp2  = StrideArray(undef, uEltype,
                        StaticInt(nvariables(equations)), StaticInt(nnodes(analyzer)), StaticInt(nnodes(analyzer)), StaticInt(nnodes(dg)))
  x_local = StrideArray(undef, RealT,
                        StaticInt(ndims(equations)), StaticInt(nnodes(analyzer)), StaticInt(nnodes(analyzer)), StaticInt(nnodes(analyzer)))
  x_tmp1  = StrideArray(undef, RealT,
                        StaticInt(ndims(equations)), StaticInt(nnodes(analyzer)), StaticInt(nnodes(dg)), StaticInt(nnodes(dg)))
  x_tmp2  = StrideArray(undef, RealT,
                        StaticInt(ndims(equations)), StaticInt(nnodes(analyzer)), StaticInt(nnodes(analyzer)), StaticInt(nnodes(dg)))
  jacobian_local = StrideArray(undef, RealT,
                                StaticInt(nnodes(analyzer)), StaticInt(nnodes(analyzer)), StaticInt(nnodes(analyzer)))
  jacobian_tmp1  = StrideArray(undef, RealT,
                               StaticInt(nnodes(analyzer)), StaticInt(nnodes(dg)), StaticInt(nnodes(dg)))
  jacobian_tmp2  = StrideArray(undef, RealT,
                               StaticInt(nnodes(analyzer)), StaticInt(nnodes(analyzer)), StaticInt(nnodes(dg)))

  return (; u_local, u_tmp1, u_tmp2, x_local, x_tmp1, x_tmp2, jacobian_local, jacobian_tmp1, jacobian_tmp2)
end

function calc_error_norms(func, u, t, analyzer,
                          mesh::TreeMesh{3}, equations, initial_condition,
                          dg::DGSEM, cache, cache_analysis)
  @unpack vandermonde, weights = analyzer
  @unpack node_coordinates = cache.elements
  @unpack u_local, u_tmp1, u_tmp2, x_local, x_tmp1, x_tmp2 = cache_analysis

  # Set up data structures
  l2_error   = zero(func(get_node_vars(u, equations, dg, 1, 1, 1, 1), equations))
  linf_error = copy(l2_error)

  # Iterate over all elements for error calculations
  for element in eachelement(dg, cache)
    # Interpolate solution and node locations to analysis nodes
    multiply_dimensionwise!(u_local, vandermonde, view(u,                :, :, :, :, element), u_tmp1, u_tmp2)
    multiply_dimensionwise!(x_local, vandermonde, view(node_coordinates, :, :, :, :, element), x_tmp1, x_tmp2)

    # Calculate errors at each analysis node
    volume_jacobian_ = volume_jacobian(element, mesh, cache)

    for k in eachnode(analyzer), j in eachnode(analyzer), i in eachnode(analyzer)
      u_exact = initial_condition(get_node_coords(x_local, equations, dg, i, j, k), t, equations)
      diff = func(u_exact, equations) - func(get_node_vars(u_local, equations, dg, i, j, k), equations)
      l2_error += diff.^2 * (weights[i] * weights[j] * weights[k] * volume_jacobian_)
      linf_error = @. max(linf_error, abs(diff))
    end
  end

  # For L2 error, divide by total volume
  total_volume_ = total_volume(mesh)
  l2_error = @. sqrt(l2_error / total_volume_)

  return l2_error, linf_error
end

function calc_error_norms(func, u, t, analyzer,
                          mesh::CurvedMesh{3}, equations, initial_condition,
                          dg::DGSEM, cache, cache_analysis)
  @unpack vandermonde, weights = analyzer
  @unpack node_coordinates, inverse_jacobian = cache.elements
  @unpack u_local, u_tmp1, u_tmp2, x_local, x_tmp1, x_tmp2, jacobian_local, jacobian_tmp1, jacobian_tmp2 = cache_analysis

  # Set up data structures
  l2_error   = zero(func(get_node_vars(u, equations, dg, 1, 1, 1, 1), equations))
  linf_error = copy(l2_error)
  total_volume = zero(real(mesh))

  # Iterate over all elements for error calculations
  for element in eachelement(dg, cache)
    # Interpolate solution and node locations to analysis nodes
    multiply_dimensionwise!(u_local, vandermonde, view(u,                :, :, :, :, element), u_tmp1, u_tmp2)
    multiply_dimensionwise!(x_local, vandermonde, view(node_coordinates, :, :, :, :, element), x_tmp1, x_tmp2)
    multiply_scalar_dimensionwise!(jacobian_local, vandermonde, inv.(view(inverse_jacobian, :, :, :, element)), jacobian_tmp1, jacobian_tmp2)

    # Calculate errors at each analysis node
    @. jacobian_local = abs(jacobian_local)

    for k in eachnode(analyzer), j in eachnode(analyzer), i in eachnode(analyzer)
      u_exact = initial_condition(get_node_coords(x_local, equations, dg, i, j, k), t, equations)
      diff = func(u_exact, equations) - func(get_node_vars(u_local, equations, dg, i, j, k), equations)
      l2_error += diff.^2 * (weights[i] * weights[j] * weights[k] * jacobian_local[i, j, k])
      linf_error = @. max(linf_error, abs(diff))
      total_volume += weights[i] * weights[j] * weights[k] * jacobian_local[i, j, k]
    end
  end

  # For L2 error, divide by total volume
  l2_error = @. sqrt(l2_error / total_volume)

  return l2_error, linf_error
end

function integrate_via_indices(func::Func, u,
                               mesh::TreeMesh{3}, equations, dg::DGSEM, cache,
                               args...; normalize=true) where {Func}
  @unpack weights = dg.basis

  # Initialize integral with zeros of the right shape
  integral = zero(func(u, 1, 1, 1, 1, equations, dg, args...))

  # Use quadrature to numerically integrate over entire domain
  for element in eachelement(dg, cache)
    volume_jacobian_ = volume_jacobian(element, mesh, cache)
    for k in eachnode(dg), j in eachnode(dg), i in eachnode(dg)
      integral += volume_jacobian_ * weights[i] * weights[j] * weights[k] * func(u, i, j, k, element, equations, dg, args...)
    end
  end

  # Normalize with total volume
  if normalize
    integral = integral / total_volume(mesh)
  end

  return integral
end

function integrate_via_indices(func::Func, u,
                               mesh::CurvedMesh{3}, equations, dg::DGSEM, cache,
                               args...; normalize=true) where {Func}
  @unpack weights = dg.basis

  # Initialize integral with zeros of the right shape
  integral = zero(func(u, 1, 1, 1, 1, equations, dg, args...))
  total_volume = zero(real(mesh))

  # Use quadrature to numerically integrate over entire domain
  for element in eachelement(dg, cache)
    for k in eachnode(dg), j in eachnode(dg), i in eachnode(dg)
      volume_jacobian = abs(inv(cache.elements.inverse_jacobian[i, j, k, element]))
      integral += volume_jacobian * weights[i] * weights[j] * weights[k] * func(u, i, j, k, element, equations, dg, args...)
      total_volume += volume_jacobian * weights[i] * weights[j] * weights[k]
    end
  end

  # Normalize with total volume
  if normalize
    integral = integral / total_volume
  end

  return integral
end

function integrate(func::Func, u,
                   mesh::Union{TreeMesh{3},CurvedMesh{3}},
                   equations, dg::DGSEM, cache; normalize=true) where {Func}
  integrate_via_indices(u, mesh, equations, dg, cache; normalize=normalize) do u, i, j, k, element, equations, dg
    u_local = get_node_vars(u, equations, dg, i, j, k, element)
    return func(u_local, equations)
  end
end

function analyze(::typeof(entropy_timederivative), du, u, t,
                 mesh::Union{TreeMesh{3},CurvedMesh{3}}, equations, dg::DG, cache)
  # Calculate ∫(∂S/∂u ⋅ ∂u/∂t)dΩ
  integrate_via_indices(u, mesh, equations, dg, cache, du) do u, i, j, k, element, equations, dg, du
    u_node  = get_node_vars(u,  equations, dg, i, j, k, element)
    du_node = get_node_vars(du, equations, dg, i, j, k, element)
    dot(cons2entropy(u_node, equations), du_node)
  end
end

function analyze(::Val{:l2_divb}, du, u, t,
                 mesh::TreeMesh{3}, equations::IdealGlmMhdEquations3D,
                 dg::DG, cache)
  integrate_via_indices(u, mesh, equations, dg, cache, cache, dg.basis.derivative_matrix) do u, i, j, k, element, equations, dg, cache, derivative_matrix
    divb = zero(eltype(u))
    for l in eachnode(dg)
      divb += ( derivative_matrix[i, l] * u[6, l, j, k, element] +
                derivative_matrix[j, l] * u[7, i, l, k, element] +
                derivative_matrix[k, l] * u[7, i, j, l, element] )
    end
    divb *= cache.elements.inverse_jacobian[element]
    divb^2
  end |> sqrt
end

function analyze(::Val{:linf_divb}, du, u, t,
                 mesh::TreeMesh{3}, equations::IdealGlmMhdEquations3D,
                 dg::DG, cache)
  @unpack derivative_matrix, weights = dg.basis

  # integrate over all elements to get the divergence-free condition errors
  linf_divb = zero(eltype(u))
  for element in eachelement(dg, cache)
    for k in eachnode(dg), j in eachnode(dg), i in eachnode(dg)
      divb = zero(eltype(u))
      for l in eachnode(dg)
        divb += ( derivative_matrix[i, l] * u[6, l, j, k, element] +
                  derivative_matrix[j, l] * u[7, i, l, k, element] +
                  derivative_matrix[k, l] * u[7, i, j, l, element] )
      end
      divb *= cache.elements.inverse_jacobian[element]
      linf_divb = max(linf_divb, abs(divb))
    end
  end

  return linf_divb
end


# Data tables from the specificatioms

"""
Allowed characters for `Alphanumeric()` mode and their number.
"""
const alphanumeric = Dict{AbstractChar, Int64}(
  zip(vcat('0':'9', 'A':'Z', collect(" %*+-./:\$")), 0:44))

"""
Number of characters allowed for a given mode, error correction level and
version.
"""
const characterscapacity = Dict{Tuple{ErrCorrLevel, Mode}, Array{Int64, 1}}(
    (Low(), Numeric()) =>
        [41, 77, 127, 187, 255, 322, 370, 461, 552, 652, 772, 883, 1022, 1101,
         1250, 1408, 1548, 1725, 1903, 2061, 2232, 2409, 2620, 2812, 3057, 3283,
         3517, 3669, 3909, 4158, 4417, 4686, 4965, 5253, 5529, 5836, 6153, 6479,
         6743, 7089]
  , (Low(), Alphanumeric()) =>
        [25, 47, 77, 114, 154, 195, 224, 279, 335, 395, 468, 535, 619, 667, 758,
         854, 938, 1046, 1153, 1249, 1352, 1460, 1588, 1704, 1853, 1990, 2132,
         2223, 2369, 2520, 2677, 2840, 3009, 3183, 3351, 3537, 3729, 3927, 4087,
         4296]
  , (Low(), Byte()) =>
        [17, 32, 53, 78, 106, 134, 154, 192, 230, 271, 321, 367, 425, 458, 520,
         586, 644, 718, 792, 858, 929, 1003, 1091, 1171, 1273, 1367, 1465, 1528,
         1628, 1732, 1840, 1952, 2068, 2188, 2303, 2431, 2563, 2699, 2809, 2953]
  , (Medium(), Numeric()) =>
        [34, 63, 101, 149, 202, 255, 293, 365, 432, 513, 604, 691, 796, 871,
         991, 1082, 1212, 1346, 1500, 1600, 1708, 1872, 2059, 2188, 2395, 2544,
         2701, 2857, 3035, 3289, 3486, 3693, 3909, 4134, 4343, 4588, 4775, 5039,
         5313, 5596]
  , (Medium(), Alphanumeric()) =>
        [20, 38, 61, 90, 122, 154, 178, 221, 262, 311, 366, 419, 483, 528, 600,
         656, 734, 816, 909, 970, 1035, 1134, 1248, 1326, 1451, 1542, 1637,
         1732, 1839, 1994, 2113, 2238, 2369, 2506, 2632, 2780, 2894, 3054, 3220,
         3391]
  , (Medium(), Byte()) =>
        [14, 26, 42, 62, 84, 106, 122, 152, 180, 213, 251, 287, 331, 362, 412,
         450, 504, 560, 624, 666, 711, 779, 857, 911, 997, 1059, 1125, 1190,
         1264, 1370, 1452, 1538, 1628, 1722, 1809, 1911, 1989, 2099, 2213, 2331]
  , (Quartile(), Numeric()) =>
        [27, 48, 77, 111, 144, 178, 207, 259, 312, 364, 427, 489, 580, 621, 703,
         775, 876, 948, 1063, 1159, 1224, 1358, 1468, 1588, 1718, 1804, 1933,
         2085, 2181, 2358, 2473, 2670, 2805, 2949, 3081, 3244, 3417, 3599, 3791,
         3993]
  , (Quartile(), Alphanumeric()) =>
        [16, 29, 47, 67, 87, 108, 125, 157, 189, 221, 259, 296, 352, 376, 426,
         470, 531, 574, 644, 702, 742, 823, 890, 963, 1041, 1094, 1172, 1263,
         1322, 1429, 1499, 1618, 1700, 1787, 1867, 1966, 2071, 2181, 2298, 2420]
  , (Quartile(), Byte()) =>
        [11, 20, 32, 46, 60, 74, 86, 108, 130, 151, 177, 203, 241, 258, 292,
         322, 364, 394, 442, 482, 509, 565, 611, 661, 715, 751, 805, 868, 908,
         982, 1030, 1112, 1168, 1228, 1283, 1351, 1423, 1499, 1579, 1663]
  , (High(), Numeric()) =>
        [17, 34, 58, 82, 106, 139, 154, 202, 235, 288, 331, 374, 427, 468, 530,
         602, 674, 746, 813, 919, 969, 1056, 1108, 1228, 1286, 1425, 1501, 1581,
         1677, 1782, 1897, 2022, 2157, 2301, 2361, 2524, 2625, 2735, 2927, 3057]
  , (High(), Alphanumeric()) =>
        [10, 20, 35, 50, 64, 84, 93, 122, 143, 174, 200, 227, 259, 283, 321,
         365, 408, 452, 493, 557, 587, 640, 672, 744, 779, 864, 910, 958, 1016,
         1080, 1150, 1226, 1307, 1394, 1431, 1530, 1591, 1658, 1774, 1852]
  , (High(), Byte()) =>
        [7, 14, 24, 34, 44, 58, 64, 84, 98, 119, 137, 155, 177, 194, 220, 250,
         280, 310, 338, 382, 403, 439, 461, 511, 535, 593, 625, 658, 698, 742,
         790, 842, 898, 958, 983, 1051, 1093, 1139, 1219, 1273]
  )

"""
Mode indicators.
"""
const modeindicators = Dict{Mode, BitArray{1}}(
    Numeric()      => BitArray([0, 0, 0, 1])
  , Alphanumeric() => BitArray([0, 0, 1, 0])
  , Byte()         => BitArray([0, 1, 0, 0])
  )

"""
Character count length for different mode and version groups.
"""
const charactercountlength = Dict{Mode, Array{Int64, 1}}(
    Numeric()      => [10, 12, 14]
  , Alphanumeric() => [9, 11, 13]
  , Byte()         => [8, 16, 16]
  )

"""
Information about the error correction codeblocks per level and version.
"""
const ecblockinfo = Dict{ErrCorrLevel,Array{Int64,2}}(
    Low() =>
      [7 1 19 0 0; 10 1 34 0 0; 15 1 55 0 0; 20 1 80 0 0; 26 1 108 0 0;
       18 2 68 0 0; 20 2 78 0 0; 24 2 97 0 0; 30 2 116 0 0; 18 2 68 2 69;
       20 4 81 0 0; 24 2 92 2 93; 26 4 107 0 0; 30 3 115 1 116; 22 5 87 1 88;
       24 5 98 1 99; 28 1 107 5 108; 30 5 120 1 121; 28 3 113 4 114;
       28 3 107 5 108; 28 4 116 4 117; 28 2 111 7 112; 30 4 121 5 122;
       30 6 117 4 118; 26 8 106 4 107; 28 10 114 2 115; 30 8 122 4 123;
       30 3 117 10 118; 30 7 116 7 117; 30 5 115 10 116; 30 13 115 3 116;
       30 17 115 0 0; 30 17 115 1 116; 30 13 115 6 116; 30 12 121 7 122;
       30 6 121 14 122; 30 17 122 4 123; 30 4 122 18 123; 30 20 117 4 118;
       30 19 118 6 119]
  , Medium() =>
    [10 1 16 0 0; 16 1 28 0 0; 26 1 44 0 0; 18 2 32 0 0; 24 2 43 0 0;
     16 4 27 0 0; 18 4 31 0 0; 22 2 38 2 39; 22 3 36 2 37; 26 4 43 1 44;
     30 1 50 4 51; 22 6 36 2 37; 22 8 37 1 38; 24 4 40 5 41; 24 5 41 5 42;
     28 7 45 3 46; 28 10 46 1 47; 26 9 43 4 44; 26 3 44 11 45; 26 3 41 13 42;
     26 17 42 0 0; 28 17 46 0 0; 28 4 47 14 48; 28 6 45 14 46; 28 8 47 13 48;
     28 19 46 4 47; 28 22 45 3 46; 28 3 45 23 46; 28 21 45 7 46; 28 19 47 10 48;
     28 2 46 29 47; 28 10 46 23 47; 28 14 46 21 47; 28 14 46 23 47;
     28 12 47 26 48; 28 6 47 34 48; 28 29 46 14 47; 28 13 46 32 47;
     28 40 47 7 48; 28 18 47 31 48]
  , Quartile() =>
      [13 1 13 0 0; 22 1 22 0 0; 18 2 17 0 0; 26 2 24 0 0; 18 2 15 2 16;
       24 4 19 0 0; 18 2 14 4 15; 22 4 18 2 19; 20 4 16 4 17; 24 6 19 2 20;
       28 4 22 4 23; 26 4 20 6 21; 24 8 20 4 21; 20 11 16 5 17; 30 5 24 7 25;
       24 15 19 2 20; 28 1 22 15 23; 28 17 22 1 23; 26 17 21 4 22;
       30 15 24 5 25; 28 17 22 6 23; 30 7 24 16 25; 30 11 24 14 25;
       30 11 24 16 25; 30 7 24 22 25; 28 28 22 6 23; 30 8 23 26 24;
       30 4 24 31 25; 30 1 23 37 24; 30 15 24 25 25; 30 42 24 1 25;
       30 10 24 35 25; 30 29 24 19 25; 30 44 24 7 25; 30 39 24 14 25;
       30 46 24 10 25; 30 49 24 10 25; 30 48 24 14 25; 30 43 24 22 25;
       30 34 24 34 25]
  , High() =>
      [17 1 9 0 0; 28 1 16 0 0; 22 2 13 0 0; 16 4 9 0 0; 22 2 11 2 12;
       28 4 15 0 0; 26 4 13 1 14; 26 4 14 2 15; 24 4 12 4 13; 28 6 15 2 16;
       24 3 12 8 13; 28 7 14 4 15; 22 12 11 4 12; 24 11 12 5 13; 24 11 12 7 13;
       30 3 15 13 16; 28 2 14 17 15; 28 2 14 19 15; 26 9 13 16 14;
       28 15 15 10 16; 30 19 16 6 17; 24 34 13 0 0; 30 16 15 14 16;
       30 30 16 2 17; 30 22 15 13 16; 30 33 16 4 17; 30 12 15 28 16;
       30 11 15 31 16; 30 19 15 26 16; 30 23 15 25 16; 30 23 15 28 16;
       30 19 15 35 16; 30 11 15 46 16; 30 59 16 1 17; 30 22 15 41 16;
       30 2 15 64 16; 30 24 15 46 16; 30 42 15 32 16; 30 10 15 67 16;
       30 20 15 61 16]
  )

"""
Remainder bits per version.
"""
const remainderbits = Array{Int64, 1}(
    [0, 7, 7, 7, 7, 7, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3,
     4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0])

"""
Location of the alignment patterns per version.
"""
const alignmentlocation = Array{Array{Int64, 1}, 1}(
    [ []
    , [6, 18]
    , [6, 22]
    , [6, 26]
    , [6, 30]
    , [6, 34]
    , [6, 22, 38]
    , [6, 24, 42]
    , [6, 26, 46]
    , [6, 28, 50]
    , [6, 30, 54]
    , [6, 32, 58]
    , [6, 34, 62]
    , [6, 26, 46, 66]
    , [6, 26, 48, 70]
    , [6, 26, 50, 74]
    , [6, 30, 54, 78]
    , [6, 30, 56, 82]
    , [6, 30, 58, 86]
    , [6, 34, 62, 90]
    , [6, 28, 50, 72, 94]
    , [6, 26, 50, 74, 98]
    , [6, 30, 54, 78, 102]
    , [6, 28, 54, 80, 106]
    , [6, 32, 58, 84, 110]
    , [6, 30, 58, 86, 114]
    , [6, 34, 62, 90, 118]
    , [6, 26, 50, 74, 98, 122]
    , [6, 30, 54, 78, 102, 126]
    , [6, 26, 52, 78, 104, 130]
    , [6, 30, 56, 82, 108, 134]
    , [6, 34, 60, 86, 112, 138]
    , [6, 30, 58, 86, 114, 142]
    , [6, 34, 62, 90, 118, 146]
    , [6, 30, 54, 78, 102, 126, 150]
    , [6, 24, 50, 76, 102, 128, 154]
    , [6, 28, 54, 80, 106, 132, 158]
    , [6, 32, 58, 84, 110, 136, 162]
    , [6, 26, 54, 82, 110, 138, 166]
    , [6, 30, 58, 86, 114, 142, 170]
    ]
  )

"""
Format information per mode and mask number.
"""
const formatinfo = Dict{Tuple{ErrCorrLevel, Int64}, BitArray{1}}(
    [ (Low(), 0) => [1,1,1,0,1,1,1,1,1,0,0,0,1,0,0]
    , (Low(), 1) => [1,1,1,0,0,1,0,1,1,1,1,0,0,1,1]
    , (Low(), 2) => [1,1,1,1,1,0,1,1,0,1,0,1,0,1,0]
    , (Low(), 3) => [1,1,1,1,0,0,0,1,0,0,1,1,1,0,1]
    , (Low(), 4) => [1,1,0,0,1,1,0,0,0,1,0,1,1,1,1]
    , (Low(), 5) => [1,1,0,0,0,1,1,0,0,0,1,1,0,0,0]
    , (Low(), 6) => [1,1,0,1,1,0,0,0,1,0,0,0,0,0,1]
    , (Low(), 7) => [1,1,0,1,0,0,1,0,1,1,1,0,1,1,0]
    , (Medium(), 0) => [1,0,1,0,1,0,0,0,0,0,1,0,0,1,0]
    , (Medium(), 1) => [1,0,1,0,0,0,1,0,0,1,0,0,1,0,1]
    , (Medium(), 2) => [1,0,1,1,1,1,0,0,1,1,1,1,1,0,0]
    , (Medium(), 3) => [1,0,1,1,0,1,1,0,1,0,0,1,0,1,1]
    , (Medium(), 4) => [1,0,0,0,1,0,1,1,1,1,1,1,0,0,1]
    , (Medium(), 5) => [1,0,0,0,0,0,0,1,1,0,0,1,1,1,0]
    , (Medium(), 6) => [1,0,0,1,1,1,1,1,0,0,1,0,1,1,1]
    , (Medium(), 7) => [1,0,0,1,0,1,0,1,0,1,0,0,0,0,0]
    , (Quartile(), 0) => [0,1,1,0,1,0,1,0,1,0,1,1,1,1,1]
    , (Quartile(), 1) => [0,1,1,0,0,0,0,0,1,1,0,1,0,0,0]
    , (Quartile(), 2) => [0,1,1,1,1,1,1,0,0,1,1,0,0,0,1]
    , (Quartile(), 3) => [0,1,1,1,0,1,0,0,0,0,0,0,1,1,0]
    , (Quartile(), 4) => [0,1,0,0,1,0,0,1,0,1,1,0,1,0,0]
    , (Quartile(), 5) => [0,1,0,0,0,0,1,1,0,0,0,0,0,1,1]
    , (Quartile(), 6) => [0,1,0,1,1,1,0,1,1,0,1,1,0,1,0]
    , (Quartile(), 7) => [0,1,0,1,0,1,1,1,1,1,0,1,1,0,1]
    , (High(), 0) => [0,0,1,0,1,1,0,1,0,0,0,1,0,0,1]
    , (High(), 1) => [0,0,1,0,0,1,1,1,0,1,1,1,1,1,0]
    , (High(), 2) => [0,0,1,1,1,0,0,1,1,1,0,0,1,1,1]
    , (High(), 3) => [0,0,1,1,0,0,1,1,1,0,1,0,0,0,0]
    , (High(), 4) => [0,0,0,0,1,1,1,0,1,1,0,0,0,1,0]
    , (High(), 5) => [0,0,0,0,0,1,0,0,1,0,1,0,1,0,1]
    , (High(), 6) => [0,0,0,1,1,0,1,0,0,0,0,1,1,0,0]
    , (High(), 7) => [0,0,0,1,0,0,0,0,0,1,1,1,0,1,1]
    ]
  )

"""
Version information.
"""
const versioninfo = Array{Array{Bool, 1}, 1}(
    [ []
    , []
    , []
    , []
    , []
    , []
    , [0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0]
    , [0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0]
    , [1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0]
    , [1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0]
    , [0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0]
    , [0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0]
    , [1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0]
    , [1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0]
    , [0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0]
    , [0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0]
    , [1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0]
    , [1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0]
    , [0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0]
    , [0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0]
    , [1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0]
    , [1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0]
    , [0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0]
    , [0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0]
    , [1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0]
    , [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0]
    , [0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0]
    , [0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0]
    , [1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0]
    , [1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0]
    , [0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0]
    , [1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1]
    , [0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1]
    , [0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1]
    , [1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1]
    , [1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1]
    , [0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1]
    , [0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1]
    , [1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1]
    , [1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1]
    ]
  )


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

"""
Problem u(X) = u₀ in Γ(d)
"""
mutable struct Dirichlet <: BoundaryProblem
    formulation :: Symbol
    variational :: Bool
    dual_basis :: Bool
    order :: Int
end

function Dirichlet()
    Dirichlet(:incremental, false, false, 1)
end

""" Return dual basis transformation matrix Ae. """
function get_dualbasis(element::Element, time::Float64, order=1)
    nnodes = length(element)
    De = zeros(nnodes, nnodes)
    Me = zeros(nnodes, nnodes)
    for ip in get_integration_points(element, order)
        detJ = element(ip, time, Val{:detJ})
        w = ip.weight*detJ
        N = element(ip, time)
        De += w*Matrix(Diagonal(vec(N)))
        Me += w*N'*N
    end
    return De, Me, De*inv(Me)
end

function get_formulation_type(problem::Problem{Dirichlet})
    return problem.properties.formulation
end

function assemble!(problem::Problem{Dirichlet}, time::Float64=0.0;
                   auto_initialize=true)
    # FIXME: boilerplate
    if !isempty(problem.assembly)
        @warn("Assemble problem $(problem.name): problem.assembly is not empty and assembling, are you sure you know what are you doing?")
    end
    if isempty(problem.elements)
        @warn("Assemble problem $(problem.name): problem.elements is empty, no elements in problem?")
    else
        first_element = first(problem.elements)
        unknown_field_name = get_unknown_field_name(problem)
        if !haskey(first_element, unknown_field_name)
            @warn("Assemble problem $(problem.name): seems that problem is uninitialized.")
            if auto_initialize
                @info("Initializing problem $(problem.name) at time $time automatically.")
                initialize!(problem, time)
            end
        end
    end

    if hasmethod(assemble_prehook!, Tuple{typeof(problem), Float64})
        assemble_prehook!(problem, time)
    end

    if problem.properties.variational
        for element in get_elements(problem)
            assemble!(problem.assembly, problem, element, time)
        end
    else # nodal collocation
        field_vals = Dict{Int64, Float64}()
        field_name = get_parent_field_name(problem)
        field_dim = get_unknown_field_dimension(problem)
        for element in get_elements(problem)
            gdofs = get_gdofs(problem, element)
            for i=1:field_dim
                haskey(element, field_name*" $i") || continue
                ldofs = gdofs[i:field_dim:end]
                xis = get_reference_coordinates(element)
                vals = Float64[]
                for xi in xis
                    g = element(field_name*" $i", xi, time)
                    # u = u_prev + Δu ⇒ Δu = u - u_prev
                    if haskey(element, field_name)
                        g_prev = element(field_name, xi, time)
                        g -= g_prev[i]
                    end
                    push!(vals, g)
                end
                for (dof, g) in zip(ldofs, vals)
                    field_vals[dof] = g
                end
            end
        end
        for (k, v) in field_vals
            FEMBase.add!(problem.assembly.C1, k, k, 1.0)
            FEMBase.add!(problem.assembly.C2, k, k, 1.0)
            FEMBase.add!(problem.assembly.g, k, 1, v)
        end
    end

    if hasmethod(assemble_posthook!, Tuple{typeof(problem), Float64})
        assemble_posthook!(problem, time)
    end
end

function assemble!(assembly::Assembly, problem::Problem{Dirichlet},
                   element::Element, time::Float64)

    # get dimension and name of PARENT field
    nnodes = length(element)
    field_dim = get_unknown_field_dimension(problem)
    field_name = get_parent_field_name(problem)
    gdofs = get_gdofs(problem, element)
    props = problem.properties

    if problem.properties.dual_basis
        De, Me, Ae = get_dualbasis(element, time)
    else
        Ae = I
        De = zeros(nnodes, nnodes)
        for ip in get_integration_points(element, props.order)
            N = element(ip, time)
            detJ = element(ip, time, Val{:detJ})
            De += ip.weight*N'*N*detJ
        end
    end

    # left hand side
    for i=1:field_dim
        ldofs = gdofs[i:field_dim:end]
        if haskey(element, field_name*" $i")
            add!(assembly.C1, ldofs, ldofs, De)
            add!(assembly.C2, ldofs, ldofs, De)
        end
    end

    # right hand side
    for ip in get_integration_points(element, props.order)
        detJ = element(ip, time, Val{:detJ})
        w = ip.weight*detJ
        N = element(ip, time)

        for i=1:field_dim
            ldofs = gdofs[i:field_dim:end]
            if haskey(element, field_name*" $i")
                g = element(field_name*" $i", ip, time)
                # u = u_prev + Δu ⇒ Δu = u - u_prev
                if haskey(element, field_name)
                    g_prev = element(field_name, ip, time)
                    g -= g_prev[i]
                end
                add!(assembly.g, ldofs, w*g*Ae*N')
            end
        end

    end

end

function postprocess!(problem::Problem{Dirichlet}, time::Float64, ::Type{Val{Symbol("reaction force")}})
    la = problem("lambda", time)
    rf = Dict(nid => -lai for (nid, lai) in la)
    update!(problem, "reaction force", time => rf)
end


# License for this file: MIT (expat)
# Copyright 2017-2018, DLR Institute of System Dynamics and Control
#
# This file is part of module
#   ModiaMath.NonlinearEquations (ModiaMath/NonlinearEquations/_module.jl)
#

"""
    module KINSOL - Solve nonlinear equation system with Sundials KINSOL

The goal is to solve the same system several times with KINSOL.
"""
module KINSOL

import Sundials
import ModiaMath
using LinearAlgebra

@noinline function old_cfunction(f, r, a)
    ccall(:jl_function_ptr, Ptr{Cvoid}, (Any, Any, Any), f, r, a)
end

mutable struct NonlinearEquationsInfo
    extraInfo                # Model-specific extra information
    name::String             # Name of equation system
    ny::Int                  # Number of equations (length of y-vector)
    getResidues!::Function   # Function of the nonlinear equation system
    y0::Vector{Float64}      # The initial y vector (to print in error messages)
    lastNorm_r::Float64
    lastrScaledNorm_r::Float64
    kin_mem::Ptr{Cvoid}      # KINSOL pointer (to access all KINgetXXX functions)

    function NonlinearEquationsInfo(name::String, ny::Int, getResidues!::Function; extraInfo=nothing)
        @assert(ny >= 0)
        new(extraInfo, name, ny, getResidues!, zeros(ny), 1.0, 1.0)
    end
end

function kinsol_f(y::Sundials.N_Vector, r::Sundials.N_Vector, eqInfo::NonlinearEquationsInfo)
    eqInfo.getResidues!(eqInfo, Sundials.asarray(y), Sundials.asarray(r))
    return Cint(0)   # indicates normal return
end

@static if VERSION < v"0.7.0-DEV.2005"
    const kinsol_fc = cfunction(kinsol_f, Cint, (Sundials.N_Vector, Sundials.N_Vector, Ref{NonlinearEquationsInfo}))
end

function kinsol_ErrHandlerFn(error_code::Cint, KINmodule::Cstring, KINfunction::Cstring,
                             message::Cstring, eqInfo::NonlinearEquationsInfo)
    if error_code > 0
        # Print information text
        println("\n\n!!! Warning from ModiaMath.NonlinearEquations.KINSOL: ", unsafe_string(KINfunction),
               "(...) returned with a [", unsafe_string(KINmodule), "] error_code  = ",
               error_code, ":\n", unsafe_string(message), "\n")
    else
        simulationModel = eqInfo.extraInfo
        tables = ModiaMath.getVariableAndResidueValues(simulationModel)

        if tables != nothing
            println("\n\nLast used values in model:\n\n",
                  tables[1], "\n\n",
                  tables[2], "\n\n")
        end

        if error_code == -11
            str1 = "\nIt might be that the Jacobian is singular (= there are redundant equations).\n"
        else
            str1 = "\n"
        end

        if typeof(simulationModel) <: ModiaMath.AbstractSimulationModel
            simState = simulationModel.simulationState
            x_names     = String[simState.getVariableName(simState.model, ModiaMath.DAE.Category_X, i)  for i = 1:simState.nx]

            error("\n\n!!! Error from ModiaMath.NonlinearEquations.KINSOL: ", unsafe_string(KINfunction),
                  "(...) returned with a [", unsafe_string(KINmodule), "] error:\n    ",
                  unsafe_string(message), "\nModiaMath info:\nlastNorm(r) = ", eqInfo.lastNorm_r,
                  ", lastNorm(rScaled*r) = ", eqInfo.lastrScaledNorm_r, ".", str1,
                  string(simState.name), ": time = " , string(simState.time) ,
                 ", stepsize of implicit Euler step = " , string(simState.hev) ,
                 ", scaleConstraintsAtEvents = " , string(simState.scaleConstraintsAtEvents) ,
                 "\nx_names     = " , string(x_names) ,
                 "\nx_start     = " , string(eqInfo.y0) ,
                 "\nx_fixed     = " , string(simState.x_fixed) ,
                 "\nx_nominal   = " , string(simState.x_nominal) ,
                 "\nx_yScale    = " , string(simState.yScale) ,
                 "\nx_rScale    = " , string(simState.rScale) ,
                 "\nx           = " , string(simState.xev) ,
                 "\nderx        = " , string(simState.derxev) ,
                 "\nresidues    = " , string(simState.residues) ,
                 "\nnx          = " , string(simState.nx) ,
                 "\nnd          = " , string(simState.nd) ,
                 "\nnc          = " , string(simState.nc) ,
                 "\nnw          = " , string(simState.nw) ,
                 "\nnz          = " , string(simState.nz) ,
                 "\ntolerance   = " , string(simState.tolerance) ,
                 "\nFTOL        = " , string(simState.FTOL))

        else
            error("\n\n!!! Error from ModiaMath.NonlinearEquations.KINSOL: ", unsafe_string(KINfunction),
                  "(...) returned with a [", unsafe_string(KINmodule), "] error:\n    ",
                  unsafe_string(message), "\nModiaMath info:\nlastNorm(r) = ", eqInfo.lastNorm_r,
                  ", lastNorm(rScaled*r) = ", eqInfo.lastrScaledNorm_r, ".", str1)
        end
    end
    return nothing
end

@static if VERSION < v"0.7.0-DEV.2005"
    const kinsol_ErrHandlerFnc = cfunction(kinsol_ErrHandlerFn, Void, (Cint, Cstring, Cstring, Cstring, Ref{NonlinearEquationsInfo}))
end

function solveNonlinearEquations!(eqInfo::NonlinearEquationsInfo, y::Vector{Float64};
                                  FTOL::Float64=eps(Float64)^(1 / 3),
                                  yScale::Vector{Float64}=ones(length(y)),
                                  rScale::Vector{Float64}=ones(length(y)))
    # Create KINSOL and info structure
    @assert(length(y) == eqInfo.ny)
    kmem = Sundials.KINCreate()
    if kmem == C_NULL
        error("ModiaMath.NonlinearEquations.KINSOL.solveNonlinearEquations!: Failed to allocate KINSOL solver object")
    end
    eqInfo.kin_mem = kmem
    eqInfo.y0 .= y

    # Run KINSOL
    try
        # Set error handler function
        @static if VERSION >= v"0.7.0-DEV.2005"
            Sundials.KINSetErrHandlerFn(kmem, old_cfunction(kinsol_ErrHandlerFn, Nothing, Tuple{Cint,Cstring,Cstring,Cstring,Ref{typeof(NonlinearEquationsInfo)}}), pointer_from_objref(eqInfo))
        else
            Sundials.KINSetErrHandlerFn(kmem, kinsol_ErrHandlerFnc, pointer_from_objref(eqInfo))
        end

        # Initialize KINSOL
        @static if VERSION >= v"0.7.0-DEV.2005"
            Sundials.KINInit(kmem, old_cfunction(kinsol_f, Cint, Tuple{Sundials.N_Vector,Sundials.N_Vector,Ref{typeof(NonlinearEquationsInfo)}}), Sundials.NVector(y))
        else
            Sundials.KINInit(kmem, kinsol_fc, y)
        end
        Sundials.KINSetUserData(kmem, eqInfo)
        Sundials.KINSetFuncNormTol(kmem, FTOL)
        Sundials.KINSetScaledStepTol(kmem, FTOL * FTOL)

        # Set maximum allowable scaled length mxnewtstep of the Newton step
        # KINSOL defines a default of mxnewtstep = 1000*norm(y.*yScale).
        # This fails if y is a zero vector. Therefore, mxnewtstep is changed to
        # take only yScale into account.
        Sundials.KINSetMaxNewtonStep(kmem, 1000.0 * norm(yScale, Inf))

        # Set linear solver
        #   nnz_jac = nnz(sim.jac)
        #   IDAKLU(mem, ny, nnz_jac)
        #   IDAKLUSetOrdering(mem, KLUorderingChoice)
        #   IDASlsSetSparseJacFn(mem);
        #else
        A = Sundials.SUNDenseMatrix(length(y), length(y))
        LS = Sundials.SUNDenseLinearSolver(Sundials.NVector(y), A)
        Sundials.KINDlsSetLinearSolver(kmem, LS, A)
        #end
        strategy = Sundials.KIN_LINESEARCH

        @static if VERSION >= v"0.7.0-DEV.2005"
            Sundials.KINSol(kmem, Sundials.NVector(y), strategy, Sundials.NVector(yScale), Sundials.NVector(rScale))
        else
            Sundials.KINSol(kmem, y, strategy, yScale, rScale)
        end

    finally
        # Free allocated memory
        Sundials.KINFree([kmem])
    end

    return nothing
end

end


function gather_bodies_initial_coordinates(simulation::NBodySimulation)
    system = simulation.system
    bodies = system.bodies;
    len = n = length(bodies)
    if simulation.thermostat isa NoseHooverThermostat
        len += 1
    end
    u0 = zeros(3, len)
    v0 = zeros(3, len)
    for i = 1:n
        u0[:, i] = bodies[i].r
        v0[:, i] = bodies[i].v
    end
    (u0, v0, n)
end

function gather_bodies_initial_coordinates(simulation::NBodySimulation{<:WaterSPCFw})
    system = simulation.system
    molecules = system.bodies
    n = length(molecules)
    len = 3*n
    if simulation.thermostat isa NoseHooverThermostat
        len = 3*n+1
    end
    (u0,v0) = gather_atom_coordinates(simulation.system, len)
    (u0, v0, n)
end

function gather_atom_coordinates(system::WaterSPCFw{<:MassBody}, len)
    molecules = system.bodies
    n = length(molecules)
    T = eltype(first(molecules).r)
    u0 = zeros(T,3,len)
    v0 = zeros(T,3,len)
    for i = 1:n
        p = system.scpfw_parameters
        indO, indH1, indH2 = 3*(i-1)+1, 3*(i-1)+2, 3*(i-1)+3
        u0[:, indO]  = molecules[i].r
        u0[:, indH1] = molecules[i].r .+ [p.rOH, 0.0, 0.0]
        u0[:, indH2] = molecules[i].r .+ [cos(p.aHOH) * p.rOH, 0.0, sin(p.aHOH) * p.rOH]
        v0[:, indO]  = molecules[i].v
        v0[:, indH1] = molecules[i].v
        v0[:, indH2] = molecules[i].v
    end
    (u0, v0)
end

function gather_atom_coordinates(system::WaterSPCFw{<:WaterMolecule}, len)
    molecules = system.bodies
    n = length(molecules)
    T = eltype(first(molecules).r)
    u0 = zeros(T,3,len)
    v0 = zeros(T,3,len)
    for i = 1:n
        p = system.scpfw_parameters
        indO, indH1, indH2 = 3*(i-1)+1, 3*(i-1)+2, 3*(i-1)+3
        u0[:, indO]  = molecules[i].O.r
        u0[:, indH1] = molecules[i].H1.r
        u0[:, indH2] = molecules[i].H2.r
        v0[:, indO]  = molecules[i].O.v
        v0[:, indH1] = molecules[i].H1.v
        v0[:, indH2] = molecules[i].H2.v
    end
    (u0, v0)
end

function gather_accelerations_for_potentials(simulation::NBodySimulation{CustomAccelerationSystem})
    acceleration_functions = []
    push!(acceleration_functions, simulation.system.acceleration)
    acceleration_functions
end

function gather_accelerations_for_potentials(simulation::NBodySimulation{<:PotentialNBodySystem})
    acceleration_functions = []
    for (potential, parameters) in simulation.system.potentials
        push!(acceleration_functions, get_accelerating_function(parameters, simulation))
    end
    acceleration_functions
end

function gather_group_accelerations(simulation::NBodySimulation{<:WaterSPCFw})
    accelerations = []
    push!(accelerations, get_group_accelerating_function(simulation.system.scpfw_parameters, simulation))
    accelerations
end

function get_accelerating_function(parameters::LennardJonesParameters, simulation::NBodySimulation)
    (ms, indxs) = obtain_data_for_lennard_jones_interaction(simulation.system)
    (dv, u, v, t, i) -> pairwise_lennard_jones_acceleration!(dv, u, i, indxs, ms, parameters, simulation.boundary_conditions)
end

function get_accelerating_function(parameters::ElectrostaticParameters, simulation::NBodySimulation)
    (qs, ms, indxs, exclude) = obtain_data_for_electrostatic_interaction(simulation.system)
    (dv, u, v, t, i) -> pairwise_electrostatic_acceleration!(dv, u, i, length(indxs), qs, ms, exclude, parameters, simulation.boundary_conditions)
end

function get_accelerating_function(parameters::SPCFwParameters, simulation::NBodySimulation)
    (ms, neighbouhood) = obtain_data_for_harmonic_bond_interaction(simulation.system, parameters)
    (dv, u, v, t, i) -> harmonic_bond_potential_acceleration!(dv, u, i, ms, neighbouhood, parameters)
end

function get_accelerating_function(parameters::GravitationalParameters, simulation::NBodySimulation)
    (dv, u, v, t, i) -> gravitational_acceleration!(dv, u, i, length(simulation.system.bodies), simulation.system.bodies, parameters)
end

function get_accelerating_function(parameters::MagnetostaticParameters, simulation::NBodySimulation)
    (dv, u, v, t, i) -> magnetostatic_dipdip_acceleration!(dv, u, i, length(simulation.system.bodies), simulation.system.bodies, parameters)
end

function get_group_accelerating_function(parameters::PotentialParameters, simulation::NBodySimulation{<:WaterSPCFw})
    (ms, indxs) = obtain_data_for_lennard_jones_interaction(simulation.system)
    (dv, u, v, t, i) -> valence_angle_potential_acceleration!(dv, u, 3*(i-1)+2, 3*(i-1)+1, 3*(i-1)+3, ms, parameters)
end

function obtain_data_for_harmonic_bond_interaction(system::WaterSPCFw, p::SPCFwParameters)
    neighbouhoods = Dict{Int, Vector{Tuple{Int,Float64}}}()
    n = length(system.bodies)
    ms = zeros(3 * n)
    for i in 1:n
        indO, indH1, indH2 = 3 * (i - 1) + 1, 3 * (i - 1) + 2, 3 * (i - 1) + 3
        ms[indO] = system.mO
        ms[indH1] = system.mH
        ms[indH2] = system.mH
        neighbours_o = Vector{Tuple{Int,Float64}}()
        push!(neighbours_o, (indH1, p.kb))
        push!(neighbours_o, (indH2, p.kb))
        neighbours_h1 = Vector{Tuple{Int,Float64}}()
        push!(neighbours_h1, (indO, p.kb))
        neighbours_h2 = Vector{Tuple{Int,Float64}}()
        push!(neighbours_h2, (indO, p.kb))
        neighbouhoods[indO] = neighbours_o
        neighbouhoods[indH1] = neighbours_h1
        neighbouhoods[indH2] = neighbours_h2
    end
    (ms, neighbouhoods)
end

function obtain_data_for_lennard_jones_interaction(system::PotentialNBodySystem)
    bodies = system.bodies
    n = length(bodies)
    ms = zeros(typeof(first(bodies).m), n)
    indxs = zeros(Int, n)
    for i = 1:n
        ms[i] = bodies[i].m
        indxs[i] = i
    end
    return (ms, indxs)
end

function obtain_data_for_lennard_jones_interaction(system::WaterSPCFw)
    bodies = system.bodies
    n = length(bodies)
    ms = zeros(3 * n)
    indxs = zeros(Int, n)
    for i = 1:n
        indxs[i] = 3 * (i - 1) + 1
        ms[3 * (i - 1) + 1] = system.mO
        ms[3 * (i - 1) + 2] = system.mH
        ms[3 * (i - 1) + 3] = system.mH
    end
    return (ms, indxs)
end

function obtain_data_for_electrostatic_interaction(system::PotentialNBodySystem)
    bodies = system.bodies
    n = length(bodies)
    qs = zeros(typeof(first(bodies).q), n)
    ms = zeros(typeof(first(bodies).m), n)
    indxs = collect(1:n)
    exclude = Dict{Int, Vector{Int}}()
    for i = 1:n
        qs[i] = bodies[i].q
        ms[i] = bodies[i].m
        exclude[i] = [i]
    end
    return (qs, ms, indxs, exclude)
end

function obtain_data_for_electrostatic_interaction(system::WaterSPCFw)
    bodies = system.bodies
    n = length(bodies)
    qs = zeros(3 * n)
    ms = zeros(3 * n)
    indxs = collect(1:3*n)
    exclude = Dict{Int, Vector{Int}}()
    for i = 1:n
        Oind = 3 * (i - 1) + 1
        qs[Oind] = system.qO
        qs[Oind + 1] = system.qH
        qs[Oind + 2] = system.qH
        ms[Oind] = system.mO
        ms[Oind + 1] = system.mH
        ms[Oind + 2] = system.mH
        exclude[Oind] = [Oind, Oind+1, Oind+2,3*n+1]
        exclude[Oind+1] = [Oind, Oind+1, Oind+2,3*n+1]
        exclude[Oind+2] = [Oind, Oind+1, Oind+2,3*n+1]
    end
    return (qs, ms, indxs, exclude)
end

function obtain_data_for_electrostatic_interaction(system::NBodySystem)
    bodies = system.bodies
    n = length(bodies)
    qs = zeros(typeof(first(bodies).m), n)
    ms = zeros(typeof(first(bodies).m), n)
    return (qs, ms)
end

function obtain_data_for_valence_angle_harmonic_interaction(system::WaterSPCFw)
    p = system.scpfw_parameters
    bonds = Vector{Tuple{Int, Int, Int, Float64, Float64}}()
    n = length(system.bodies)
    ms = zeros(3 * n)
    for i = 1:n
        indO, indH1, indH2 = 3 * (i - 1) + 1, 3 * (i - 1) + 2, 3 * (i - 1) + 3
        ms[indO] = system.mO
        ms[indH1] = system.mH
        ms[indH2] = system.mH
        push!(bonds, (indH1, indO, indH2, p.aHOH, p.ka))
    end
    return (ms, bonds)
end

function gather_simultaneous_acceleration(s::NBodySimulation)
    acelerations = []
    if s.thermostat isa BerendsenThermostat
        push!(acelerations, get_berendsen_thermostating_acceleration(s))
    elseif s.thermostat isa NoseHooverThermostat
        push!(acelerations, get_nosehoover_thermostating_acceleration(s))
    end
    acelerations
end

function get_berendsen_thermostating_acceleration(simulation::NBodySimulation)
    (ms, kb, n, nc, p) = obtain_data_for_berendsen_thermostating(simulation)
    (dv, u, v, t) -> berendsen_acceleration!(dv, v, ms, kb, n, nc, p)
end

function obtain_data_for_berendsen_thermostating(simulation::NBodySimulation)
    ms = get_masses(simulation.system)
    kb = simulation.kb
    n = length(simulation.system.bodies)
    nc = 0
    p = simulation.thermostat
    (ms, kb, n, nc, p)
end

function obtain_data_for_berendsen_thermostating(simulation::NBodySimulation{<:WaterSPCFw})
    ms = get_masses(simulation.system)
    kb = simulation.kb
    n = 3*length(simulation.system.bodies)
    nc = 2*length(simulation.system.bodies)
    p = simulation.thermostat
    (ms, kb, n, nc, p)
end

function get_nosehoover_thermostating_acceleration(simulation::NBodySimulation)
    (ms, kb, n, nc, γind, p) = obtain_data_for_nosehoover_thermostating(simulation)
    (dv, u, v, t) -> nosehoover_acceleration!(dv, u, v, ms, kb, n, nc, γind, p)
end

function obtain_data_for_nosehoover_thermostating(simulation::NBodySimulation)
    ms = get_masses(simulation.system)
    kb = simulation.kb
    n = length(simulation.system.bodies)
    γind = 3*n+1
    nc = 0
    p = simulation.thermostat
    (ms, kb, n, nc, γind, p)
end

function obtain_data_for_nosehoover_thermostating(simulation::NBodySimulation{<:WaterSPCFw})
    ms = get_masses(simulation.system)
    kb = simulation.kb
    n = 3*length(simulation.system.bodies)
    γind = 3*n+1
    nc = 2*length(simulation.system.bodies)
    p = simulation.thermostat
    (ms, kb, n, nc, γind, p)
end

function DiffEqBase.ODEProblem(simulation::NBodySimulation{<:PotentialNBodySystem})
    (u0, v0, n) = gather_bodies_initial_coordinates(simulation)

    acceleration_functions = gather_accelerations_for_potentials(simulation)

    function ode_system!(du, u, p, t)
        du[:, 1:n] = @view u[:, n + 1:2n];

        @inbounds for i = 1:n
            a = MVector(0.0, 0.0, 0.0)
            for acceleration! in acceleration_functions
                acceleration!(a, u[:, 1:n], u[:, n + 1:end], t, i);
            end
            du[:, n + i] .= a
        end
    end

    return ODEProblem(ode_system!, hcat(u0, v0), simulation.tspan)
end

function DiffEqBase.SecondOrderODEProblem(simulation::NBodySimulation{<:PotentialNBodySystem})
    (u0, v0, n) = gather_bodies_initial_coordinates(simulation)
    acceleration_functions = gather_accelerations_for_potentials(simulation)
    simultaneous_acceleration = gather_simultaneous_acceleration(simulation)

    function soode_system!(dv, v, u, p, t)
        @inbounds for i = 1:n
            a = MVector(0.0, 0.0, 0.0)
            for acceleration! in acceleration_functions
                acceleration!(a, u, v, t, i);
            end
            dv[:, i] .= a
        end
        for acceleration! in simultaneous_acceleration
            acceleration!(dv, u, v, t);
        end
    end

    SecondOrderODEProblem(soode_system!, v0, u0, simulation.tspan)
end

function DiffEqBase.SecondOrderODEProblem(simulation::NBodySimulation{<:WaterSPCFw})
    (u0, v0, n) = gather_bodies_initial_coordinates(simulation)
    (o_acelerations, h_acelerations) = gather_accelerations_for_potentials(simulation)
    group_accelerations = gather_group_accelerations(simulation)
    simultaneous_acceleration = gather_simultaneous_acceleration(simulation)
    #(ms_lj, indxs_lj) = obtain_data_for_lennard_jones_interaction(simulation.system)
    #(qs, ms_em, indxs_em, exclude) = obtain_data_for_electrostatic_interaction(simulation.system)
    #lj_parameters = simulation.system.lj_parameters
    #e_parameters  = simulation.system.e_parameters
    #pbc = simulation.boundary_conditions

    function soode_system!(dv, v, u, p, t)
        T = eltype(v)
        #=
        @inbounds for i=1:n
            aO  = MVector{3,T}(zero(T), zero(T), zero(T))
            aH1 = MVector{3,T}(zero(T), zero(T), zero(T))
            aH2 = MVector{3,T}(zero(T), zero(T), zero(T))
            pairwise_lennard_jones_acceleration!(aO,  u, 3*i-2, indxs_lj, ms_lj, lj_parameters, pbc)
            pairwise_electrostatic_acceleration!(aO,  u, 3*i-2, length(indxs_em), qs, ms_em, exclude, e_parameters, pbc)
            pairwise_electrostatic_acceleration!(aH1, u, 3*i-1, length(indxs_em), qs, ms_em, exclude, e_parameters, pbc)
            pairwise_electrostatic_acceleration!(aH2, u, 3*i-0, length(indxs_em), qs, ms_em, exclude, e_parameters, pbc)
            dv[:, 3*i-2] .= aO
            dv[:, 3*i-1] .= aH1
            dv[:, 3*i-0] .= aH2
        end
        =#
        @inbounds for i = 1:n
            a = MVector{3,T}(zero(T), zero(T), zero(T))
            for acceleration! in o_acelerations
                acceleration!(a, u, v, t, 3*i-2);
            end
            dv[:, 3*i-2] .= a
        end
        @inbounds for i ∈ 1:n, j ∈ (1,2)
            a = MVector{3,T}(zero(T), zero(T), zero(T))
            for acceleration! in h_acelerations
                acceleration!(a, u, v, t, 3*i-2+j);
            end
            dv[:, 3*i-2+j] .= a
        end
        @inbounds for i = 1:n
            for acceleration! in group_accelerations
                acceleration!(dv, u, v, t, i)
            end
        end
        for acceleration! in simultaneous_acceleration
            acceleration!(dv, u, v, t)
        end
    end
    SecondOrderODEProblem(soode_system!, v0, u0, simulation.tspan)
end

function gather_accelerations_for_potentials(simulation::NBodySimulation{<:WaterSPCFw})
    o_acelerations = []
    h_acelerations = []
    push!(o_acelerations, get_accelerating_function(simulation.system.e_parameters, simulation))
    push!(o_acelerations, get_accelerating_function(simulation.system.scpfw_parameters, simulation))
    push!(o_acelerations, get_accelerating_function(simulation.system.lj_parameters, simulation))
    push!(h_acelerations, get_accelerating_function(simulation.system.e_parameters, simulation))
    push!(h_acelerations, get_accelerating_function(simulation.system.scpfw_parameters, simulation))
    (o_acelerations, h_acelerations)
end

function DiffEqBase.SDEProblem(simulation::NBodySimulation{<:PotentialNBodySystem})
    (u0, v0, n) = gather_bodies_initial_coordinates(simulation)
    acceleration_functions = gather_accelerations_for_potentials(simulation)
    therm = simulation.thermostat

    function deterministic_acceleration!(du, u, p, t)
        du[:, 1:n] = @view u[:, n + 1:2n]
        @inbounds for i = 1:n
            a = MVector(0.0, 0.0, 0.0)
            for acceleration! in acceleration_functions
                acceleration!(a, (@view u[:, 1:n]), (@view u[:, n + 1:end]), t, i);
            end
            du[:, n + i] .= a
        end
        @. du[:, n + 1:end] -= therm.γ*u[:, n + 1:end]
    end

    σ = sqrt(2*therm.γ*simulation.kb*therm.T/simulation.system.bodies[1].m)

    function noise!(du, u, p, t)
        @. du[:, n + 1:end] += σ
    end

    return SDEProblem(deterministic_acceleration!, noise!, hcat(u0, v0), simulation.tspan)
end

function DiffEqBase.SDEProblem(simulation::NBodySimulation{<:WaterSPCFw})
    (u0, v0, n) = gather_bodies_initial_coordinates(simulation)
    (o_acelerations, h_acelerations) = gather_accelerations_for_potentials(simulation)
    group_accelerations = gather_group_accelerations(simulation)
    simultaneous_acceleration = gather_simultaneous_acceleration(simulation)
    therm = simulation.thermostat
    mH = simulation.system.mH
    mO = simulation.system.mO

    function deterministic_acceleration!(du, u, p, t)
        du[:, 1:3*n] = @view u[:, 3*n+1 : 2*3*n]
        @inbounds for i = 1:n
            a = MVector(0.0, 0.0, 0.0)
            for acceleration! in o_acelerations
                acceleration!(a, (@view u[:, 1:3*n]), (@view u[:, 3*n+1 : 2*3*n]), t, 3 * (i - 1) + 1);
            end
            du[:, 3*n+3 * (i - 1) + 1]  .= a
            @. du[:, 3*n+3 * (i - 1) + 1] -= therm.γ*u[:, 3*n+3 * (i - 1) + 1]/mO
            @. du[:, 3*n+3 * (i - 1) + 2] -= therm.γ*u[:, 3*n+3 * (i - 1) + 2]/mH
            @. du[:, 3*n+3 * (i - 1) + 3] -= therm.γ*u[:, 3*n+3 * (i - 1) + 3]/mH
        end
        @inbounds for i in 1:n, j in (2, 3)
            a = MVector(0.0, 0.0, 0.0)
            for acceleration! in h_acelerations
                acceleration!(a, (@view u[:, 1:3*n]), (@view u[:, 3*n+1 : 2*3*n]), t, 3 * (i - 1) + j);
            end
            du[:, 3*n+3 * (i - 1) + j]   .= a
        end
        @inbounds for i = 1:n
            for acceleration! in group_accelerations
                acceleration!((@view du[ :, 3*n+1 : 2*3*n]), (@view u[:, 1:3*n]), (@view u[:, 3*n+1 : 2*3*n]), t, i);
            end
        end
        for acceleration! in simultaneous_acceleration
            acceleration!((@view du[:, 3*n+1 : 2*3*n]), (@view u[:, 1:3*n]), (@view u[:, 3*n+1 : 2*3*n]), t);
        end
        @. du[:, 3*n+1 : 2*3*n] -= therm.γ*u[:, 3*n+1 : 2*3*n]
    end
    σO = sqrt(2*therm.γ*simulation.kb*therm.T)/mO
    σH = sqrt(2*therm.γ*simulation.kb*therm.T)/mH

    function noise!(du, u, p, t)
        @inbounds for i = 1:n
            @. du[:, 3*n+3 * (i - 1) + 1] += σO
            @. du[:, 3*n+3 * (i - 1) + 2] += σH
            @. du[:, 3*n+3 * (i - 1) + 3] += σH
        end
    end

    return SDEProblem(deterministic_acceleration!, noise!, hcat(u0, v0), simulation.tspan)
end