PropertyCardType = CardType{:property}()
const PropertyCard = NastranCard{PropertyCardType}

type PSHELL <: PropertyCard id::Int64
    mat1_id::Int64
    thickness::Nullable{Float64}
    nonstructural_mass::Float64
end
function convert(::Type{PSHELL},card)
    id = card[2]::Int64
    mat1_id = card[3]::Int64
    thickness = card[4] == "" ? Void : card[4]::Float64
    nonstructural_mass = card[9] == "" ? 0.0 : card[9]::Float64
    PSHELL(id,mat1_id,thickness,nonstructural_mass)
end

type PROD <: PropertyCard
    id::Int64
    mat_id::Int64
    A::Float64
    J::Float64
    C::Float64
    nonstructural_mass::Float64
end
function convert(::Type{PROD},card)
    id = card[2]::Int64
    mat_id = card[3]::Int64
    A = card[4]::Float64
    J = card[5]::Float64
    C = card[6] == "" ? 0.0 : card[6]::Float64
    nonstructural_mass = card[7] == "" ? 0.0 : card[7]::Float64
    PROD(id,mat_id,A,J,C,nonstructural_mass)
end

type PBUSH <: PropertyCard
    id::Int64
end
function convert(::Type{PBUSH},card)
    id = card[2]::Int64
    PBUSH(id)
end

type PBEAM <: PropertyCard
    id::Int64
    mat_id::Int64
    nonstructural_mass::Float64
    nonstructural_inertiaA::Float64
    nonstructural_inertiaB::Float64
end

is_continuation(field) = field == "YES"
is_double_continuation(field) = field == "YESA" || field == "NO"
function convert(::Type{PBEAM},card)
    id = card[2]::Int64
    mat_id = card[3]::Int64
    nonstructural_mass = card[9] == "" ? 0.0 : card[9]::Float64
    nonstructural_inertiaA = 0.0
    nonstructural_inertiaB = 0.0
    i = 10
    while i < length(card)
        if is_continuation(card[i])
        elseif is_double_continuation(card[i])
            i += 8
        else
            if card[i+4] != ""
                nonstructural_inertiaA = card[i+4]::Float64
            end
            if card[i+5] != ""
                nonstructural_inertiaB = card[i+5]::Float64
            end
        end
        i += 8
    end
    PBEAM(id,mat_id,nonstructural_mass,nonstructural_inertiaA,nonstructural_inertiaB)

end

type PELAS <: PropertyCard
    id::Int64
end
function convert(::Type{PELAS},card)
    id = card[2]::Int64
    PELAS(id)
end

type PAERO1 <: PropertyCard
    id::Int64
end
function convert(::Type{PAERO1},card)
    id = card[2]::Int64
    PAERO1(id)
end

type PSOLID <: PropertyCard
    id::Int64
end
function convert(::Type{PSOLID},card)
    id = card[2]::Int64
    PSOLID(id)
end


# This file is a part of Julia. License is MIT: https://julialang.org/license

__precompile__(true)

module SuiteSparse

import Base: \
import LinearAlgebra: ldiv!, rdiv!

## Functions to switch to 0-based indexing to call external sparse solvers

# Convert from 1-based to 0-based indices
function decrement!(A::AbstractArray{T}) where T<:Integer
    for i in 1:length(A); A[i] -= oneunit(T) end
    A
end
decrement(A::AbstractArray{<:Integer}) = decrement!(copy(A))

# Convert from 0-based to 1-based indices
function increment!(A::AbstractArray{T}) where T<:Integer
    for i in 1:length(A); A[i] += oneunit(T) end
    A
end
increment(A::AbstractArray{<:Integer}) = increment!(copy(A))

if Base.USE_GPL_LIBS
    include("umfpack.jl")
    include("cholmod.jl")
    include("spqr.jl")
    include("deprecated.jl")
end

end # module SuiteSparse


module TopicModelsVB

using SpecialFunctions
using Distributions
using OpenCL

export VectorList, MatrixList
export Document, Corpus
export TopicModel, GPUTopicModel, BaseTopicModel
export AbstractLDA, AbstractfLDA, AbstractCTM, AbstractfCTM, AbstractDTM, AbstractCTPF
export LDA, fLDA, CTM, fCTM, CTPF, DTM
export gpuLDA, gpuCTM, gpuCTPF
export isnegative, ispositive, logsumexp, addlogistic, partition
export checkdoc, checkcorp, readcorp, writecorp, abridgecorp!, trimcorp!, compactcorp!, padcorp!, cullcorp!, fixcorp!, showdocs, getlex, getusers
export train!, @gpu
export fixmodel!, gendoc, gencorp, showtopics, showlibs, showdrecs, showurecs

include("macros.jl")
include("utils.jl")
include("Corpus.jl")

include("TopicModel.jl")
include("GPUTopicModel.jl")

include("LDA.jl")
include("gpuLDA.jl")
include("AbstractLDA.jl")

include("fLDA.jl")
include("AbstractfLDA.jl")

include("CTM.jl")
include("gpuCTM.jl")
include("AbstractCTM.jl")

include("fCTM.jl")
include("AbstractfCTM.jl")

include("BaseTopicModel.jl")

include("DTM.jl")
include("AbstractDTM.jl")

include("CTPF.jl")
include("gpuCTPF.jl")
include("AbstractCTPF.jl")

include("modelutils.jl")

end


module FighterJets

export FighterJet

"FighterJet is a very fast vehicle with powerful weapons."
mutable struct FighterJet

    "power status: true = on, false = off"
    power::Bool

    "current direction in radians"
    direction::Float64

    "current position coordinate (x,y)"
    position::Tuple{Float64, Float64}

end

# Import generic functions
import Vehicle: power_on!, power_off!, turn!, move!, position

# Implementation of Vehicle interface

function power_on!(fj::FighterJet)
    fj.power = true
    println("Powered on: ", fj)
    nothing
end

function power_off!(fj::FighterJet)
    fj.power = false
    println("Powered off: ", fj)
    nothing
end

function turn!(fj::FighterJet, direction)
    fj.direction = direction
    println("Changed direction to ", direction, ": ", fj)
    nothing
end

function move!(fj::FighterJet, distance)
    x, y = fj.position
    dx = round(distance * cos(fj.direction), digits = 2)
    dy = round(distance * sin(fj.direction), digits = 2)
    fj.position = (x + dx, y + dy)
    println("Moved (", dx, ",", dy, "): ", fj)
    nothing
end

function position(fj::FighterJet)
    fj.position
end

end # module


@with_kw mutable struct DeepQLearningSolver{E<:ExplorationPolicy} <: Solver
    qnetwork::Any = nothing # intended to be a flux model
    learning_rate::Float32 = 1f-4
    max_steps::Int64 = 1000
    batch_size::Int64 = 32
    train_freq::Int64 = 4
    eval_freq::Int64 = 500
    target_update_freq::Int64 = 500
    num_ep_eval::Int64 = 100
    double_q::Bool = true
    dueling::Bool = true
    recurrence::Bool = false
    evaluation_policy::Any = basic_evaluation
    exploration_policy::E
    trace_length::Int64 = 40
    prioritized_replay::Bool = true
    prioritized_replay_alpha::Float32 = 0.6f0
    prioritized_replay_epsilon::Float32 = 1f-6
    prioritized_replay_beta::Float32 = 0.4f0
    buffer_size::Int64 = 1000
    max_episode_length::Int64 = 100
    train_start::Int64 = 200
    rng::AbstractRNG = MersenneTwister(0)
    logdir::Union{Nothing, String} = "log/"
    save_freq::Int64 = 3000
    log_freq::Int64 = 100
    verbose::Bool = true
end

function POMDPs.solve(solver::DeepQLearningSolver, problem::MDP)
    env = MDPCommonRLEnv{AbstractArray{Float32}}(problem) # ignores solver.rng because CommonRLEnv doesn't have rng support yet
    return solve(solver, env)
end

function POMDPs.solve(solver::DeepQLearningSolver, problem::POMDP)
    env = POMDPCommonRLEnv{AbstractArray{Float32}}(problem) # ignores solver.rng because CommonRLEnv doesn't have rng support yet
    return solve(solver, env)
end

function POMDPs.solve(solver::DeepQLearningSolver, env::AbstractEnv)
    action_map = collect(actions(env))
    action_indices = Dict(a=>i for (i, a) in enumerate(action_map))

    # check reccurence
    if isrecurrent(solver.qnetwork) && !solver.recurrence
        throw("DeepQLearningError: you passed in a recurrent model but recurrence is set to false")
    end
    replay = initialize_replay_buffer(solver, env, action_indices)
    if solver.dueling
        active_q = create_dueling_network(solver.qnetwork)
    else
        active_q = solver.qnetwork
    end
    policy = NNPolicy(env, active_q, action_map, length(obs_dimensions(env)))

    return dqn_train!(solver, env, policy, replay)
end

function dqn_train!(solver::DeepQLearningSolver, env::AbstractEnv, policy::AbstractNNPolicy, replay)
    if solver.logdir !== nothing
        logger = TBLogger(solver.logdir)
        solver.logdir = logger.logdir
    end
    active_q = getnetwork(policy) # shallow copy
    target_q = deepcopy(active_q)
    optimizer = ADAM(solver.learning_rate)
    # start training
    resetstate!(policy)
    reset!(env)
    obs = observe(env)
    done = false
    step = 0
    rtot = 0
    episode_rewards = Float64[0.0]
    episode_steps = Int64[]
    saved_mean_reward = -Inf
    scores_eval = -Inf
    model_saved = false
    eval_next = false
    save_next = false
    action_indices = Dict(a=>i for (i, a) in enumerate(actionmap(policy)))
    for t=1:solver.max_steps
        act = action(solver.exploration_policy, policy, t, obs)
        ai = action_indices[act]
        rew = act!(env, act)
        op = observe(env)
        done = terminated(env)
        exp = DQExperience(obs, ai, Float32(rew), op, done)
        if solver.recurrence
            add_exp!(replay, exp)
        elseif solver.prioritized_replay
            add_exp!(replay, exp, abs(exp.r))
        else
            add_exp!(replay, exp, 0f0)
        end
        obs = op
        step += 1
        episode_rewards[end] += rew
        if done || step >= solver.max_episode_length

            if eval_next # wait for episode to end before evaluating
                scores_eval, steps_eval, info_eval = evaluation(solver.evaluation_policy,
                policy, env,
                solver.num_ep_eval,
                solver.max_episode_length,
                solver.verbose)
                eval_next = false

                # only save after evaluation
                if save_next
                    model_saved, saved_mean_reward = save_model(solver, active_q, scores_eval, saved_mean_reward, model_saved)
                    save_next = false
                end

                if solver.logdir !== nothing
                    log_value(logger, "eval_reward", scores_eval, step = t)
                    log_value(logger, "eval_steps", steps_eval, step = t)
                    for (k, v) in info_eval
                        log_value(logger, k, v, step = t)
                    end
                end
            end

            reset!(env)
            obs = observe(env)
            resetstate!(policy)
            push!(episode_steps, step)
            push!(episode_rewards, 0.0)
            done = false
            step = 0
            rtot = 0
        end
        num_episodes = length(episode_rewards)
        avg100_reward = mean(episode_rewards[max(1, length(episode_rewards)-101):end])
        avg100_steps = mean(episode_steps[max(1, length(episode_steps)-101):end])
        if t%solver.train_freq == 0
            hs = hiddenstates(active_q)
            loss_val, grad_val = batch_train!(solver, env, policy, optimizer, target_q, replay)
            sethiddenstates!(active_q, hs)
        end

        if t%solver.target_update_freq == 0
            weights = Flux.params(active_q)
            Flux.loadparams!(target_q, weights)
        end

        if t % solver.eval_freq == 0
            eval_next = true
        end
        if t % solver.save_freq == 0
            save_next = true
        end

        if t % solver.log_freq == 0 && solver.logdir !== nothing
            nt = loginfo(solver.exploration_policy, t)
            for (k, v) in pairs(nt)
                log_value(logger, String(k), v, step=t)
            end
            if  solver.verbose
                @printf("%5d / %5d eps %0.3f |  avgR %1.3f | Loss %2.3e | Grad %2.3e | EvalR %1.3f \n",
                        t, solver.max_steps, nt[1], avg100_reward, loss_val, grad_val, scores_eval)
            end

            log_value(logger, "avg_reward", avg100_reward, step = t)
            log_value(logger, "loss", loss_val, step = t)
            log_value(logger, "grad_val", grad_val, step = t)
        end

    end # end training
    if model_saved
        if solver.verbose
            @printf("Restore model with eval reward %1.3f \n", saved_mean_reward)
            saved_model = BSON.load(joinpath(solver.logdir, "qnetwork.bson"))[:qnetwork]
            Flux.loadparams!(getnetwork(policy), saved_model)
        end
    end
    return policy
end

function initialize_replay_buffer(solver::DeepQLearningSolver, env::AbstractEnv, action_indices)
    # init and populate replay buffer
    if solver.recurrence
        replay = EpisodeReplayBuffer(env, solver.buffer_size, solver.batch_size, solver.trace_length)
    else
        replay = PrioritizedReplayBuffer(env, solver.buffer_size, solver.batch_size)
    end
    populate_replay_buffer!(replay, env, action_indices, max_pop=solver.train_start)
    return replay #XXX type unstable
end

function batch_train!(solver::DeepQLearningSolver,
                      env::AbstractEnv,
                      policy::AbstractNNPolicy,
                      optimizer,
                      target_q,
                      replay::PrioritizedReplayBuffer;
                      discount=default_discount(env)
                     )

    s_batch, a_batch, r_batch, sp_batch, done_batch, indices, importance_weights = sample(replay)

    active_q = getnetwork(policy)
    p = params(active_q)

    loss_val = nothing
    td_vals = nothing

    γ = convert(Float32, discount)
    if solver.double_q
        qp_values = active_q(sp_batch)
        target_q_values = target_q(sp_batch)
        best_a = [CartesianIndex(argmax(qp_values[:, i]), i) for i=1:solver.batch_size]
        q_sp_max = target_q_values[best_a]
    else
        q_sp_max = dropdims(maximum(target_q(sp_batch), dims=1), dims=1)
    end
    q_targets = r_batch .+ (1f0 .- done_batch) .* γ .* q_sp_max

    gs = Flux.gradient(p) do
        q_values = active_q(s_batch)
        q_sa = q_values[a_batch]
        td_vals = q_sa .- q_targets
        loss_val = sum(huber_loss, importance_weights.*td_vals)
        loss_val /= solver.batch_size
    end

    grad_norm = globalnorm(p, gs)
    Flux.Optimise.update!(optimizer, p, gs)

    if solver.prioritized_replay
        update_priorities!(replay, indices, td_vals)
    end

    return loss_val, grad_norm
end

# for RNNs
function batch_train!(solver::DeepQLearningSolver,
                      env::AbstractEnv,
                      policy::AbstractNNPolicy,
                      optimizer,
                      target_q,
                      replay::EpisodeReplayBuffer;
                      discount=default_discount(env)
                     )
    active_q = getnetwork(policy)
    s_batch, a_batch, r_batch, sp_batch, done_batch, trace_mask_batch = DeepQLearning.sample(replay)
    Flux.reset!(active_q)
    Flux.reset!(target_q)

    p = params(active_q)

    loss_val = nothing
    td_vals = nothing

    γ = convert(Float32, discount)
    q_targets = [zeros(Float32, solver.batch_size) for i=1:solver.trace_length]
    for i=1:solver.trace_length
        if solver.double_q
            qp_values = active_q(sp_batch[i])
            best_a = [CartesianIndex(argmax(qp_values[:, i]), i) for i=1:solver.batch_size]
            target_q_values = target_q(sp_batch[i])
            q_sp_max = target_q_values[best_a]
        else
            q_sp_max = dropdims(maximum(target_q(sp_batch[i]), dims=1), dims=1)
        end
        q_targets[i] .= r_batch[i] .+ (1f0 .- done_batch[i]) .* γ .* q_sp_max
    end

    Flux.reset!(active_q)

    gs = Flux.gradient(p) do
        loss_val = 0f0
        for i=1:solver.trace_length
            q_values = active_q(s_batch[i])
            q_sa = q_values[a_batch[i]]
            td_vals = q_sa .- q_targets[i]
            loss_val += sum(huber_loss, trace_mask_batch[i].*td_vals)/solver.batch_size
        end
        loss_val /= solver.trace_length
    end

    grad_norm = globalnorm(p, gs)
    Flux.Optimise.update!(optimizer, p, gs)
    return loss_val, grad_norm
end

function save_model(solver::DeepQLearningSolver, active_q, scores_eval::Float64, saved_mean_reward::Float64, model_saved::Bool)
    if scores_eval >= saved_mean_reward
        bson(joinpath(solver.logdir, "qnetwork.bson"), qnetwork=[w for w in params(active_q)])
        if solver.verbose
            @printf("Saving new model with eval reward %1.3f \n", scores_eval)
        end
        model_saved = true
        saved_mean_reward = scores_eval
    end
    return model_saved, saved_mean_reward
end

function restore_best_model(solver::DeepQLearningSolver, problem::MDP)
    env = convert(AbstractEnv, problem) # ignores solver.rng because CommonRLEnv doesn't have rng support yet
    restore_best_model(solver, env)
end

function restore_best_model(solver::DeepQLearningSolver, env::AbstractEnv)
    if solver.dueling
        active_q = create_dueling_network(solver.qnetwork)
    else
        active_q = solver.qnetwork
    end
    policy = NNPolicy(env, active_q, collect(actions(env)), length(obs_dimensions(env)))
    weights = BSON.load(solver.logdir*"qnetwork.bson")[:qnetwork]
    Flux.loadparams!(getnetwork(policy), weights)
    Flux.testmode!(getnetwork(policy))
    return policy
end

POMDPLinter.@POMDP_require POMDPs.solve(solver::DeepQLearningSolver, mdp::Union{MDP, POMDP}) begin
    P = typeof(mdp)
    S = POMDPs.statetype(P)
    A = POMDPs.actiontype(P)
    @req POMDPs.discount(::P)
    @req POMDPs.actions(::P)
    as = POMDPs.actions(mdp)
    @req length(::typeof(as))
    if isa(mdp, POMDP)
        O = obstype(mdp)
        @req POMDPs.convert_o(::Type{AbstractArray}, ::O, ::P)
    else
        @req POMDPs.convert_s(::Type{AbstractArray}, ::S, ::P)
    end
    @req POMDPs.reward(::P,::S,::A,::S)
end


"""
$(SIGNATURES)

Kinetic flux vector splitting (KFVS) flux
"""
function flux_kfvs!(
    face::Interface1F,
    ctrL::T,
    ctrR::T,
    gas::AbstractProperty,
    vs::AbstractVelocitySpace1D,
    p,
    dt = 1.0,
) where {T<:ControlVolume1F}

    dxL, dxR = p[1:2]

    flux_kfvs!(
        face.fw,
        face.ff,
        ctrL.f + ctrL.sf * dxL,
        ctrR.f - ctrR.sf * dxR,
        vs.u,
        vs.weights,
        dt,
        ctrL.sf,
        ctrR.sf,
    )

    return nothing

end

function flux_kfvs!(
    face::Interface2F,
    ctrL::T,
    ctrR::T,
    gas::AbstractProperty,
    vs::AbstractVelocitySpace1D,
    p,
    dt = 1.0,
) where {T<:ControlVolume2F}

    dxL, dxR = p[1:2]

    flux_kfvs!(
        face.fw,
        face.fh,
        face.fb,
        ctrL.h .+ ctrL.sb .* dxL,
        ctrL.b .+ ctrL.sb .* dxL,
        ctrR.h .- ctrR.sb .* dxR,
        ctrR.b .- ctrR.sb .* dxR,
        vs.u,
        vs.weights,
        dt,
        ctrL.sh,
        ctrL.sb,
        ctrR.sh,
        ctrR.sb,
    )

    return nothing

end

function flux_kfvs!(
    face::Interface1F,
    ctrL::T,
    ctrR::T,
    gas::AbstractProperty,
    vs::AbstractVelocitySpace2D,
    p,
    dt = 1.0,
) where {T<:ControlVolume1F}

    dxL, dxR, len, n, dirc = p[1:5]
    sfL = @view ctrL.sf[:, :, dirc]
    sfR = @view ctrR.sf[:, :, dirc]

    flux_kfvs!(
        face.fw,
        face.ff,
        ctrL.f + sfL * dxL,
        ctrR.f - sfR * dxR,
        vs.u .* n[1] .+ vs.v .* n[2],
        vs.v .* n[1] .- vs.u .* n[2],
        vs.weights,
        dt,
        len,
        sfL,
        sfR,
    )

    face.fw .= global_frame(face.fw, n)

    return nothing

end

function flux_kfvs!(
    face::Interface2F,
    ctrL::T,
    ctrR::T,
    gas::AbstractProperty,
    vs::AbstractVelocitySpace2D,
    p,
    dt = 1.0,
) where {T<:ControlVolume2F}

    dxL, dxR, len, n, dirc = p[1:5]
    shL = @view ctrL.sh[:, :, dirc]
    sbL = @view ctrL.sb[:, :, dirc]
    shR = @view ctrR.sh[:, :, dirc]
    sbR = @view ctrR.sb[:, :, dirc]

    flux_kfvs!(
        face.fw,
        face.fh,
        face.fb,
        ctrL.h .+ shL .* dxL,
        ctrL.b .+ sbL .* dxL,
        ctrR.h .- shR .* dxR,
        ctrR.b .- sbR .* dxR,
        vs.u .* n[1] .+ vs.v .* n[2],
        vs.v .* n[1] .- vs.u .* n[2],
        vs.weights,
        dt,
        len,
        shL,
        sbL,
        shR,
        sbR,
    )

    face.fw .= global_frame(face.fw, n)

    return nothing

end

function flux_kfvs!(
    face::Interface1F,
    ctrL::T,
    ctrR::T,
    gas::AbstractProperty,
    vs::AbstractVelocitySpace3D,
    p,
    dt = 1.0,
) where {T<:ControlVolume1F}

    dxL, dxR = p[1:2]

    if length(p) == 2 || p[3] == 1
        flux_kfvs!(
            face.fw,
            face.ff,
            ctrL.f + ctrL.sf * dxL,
            ctrR.f - ctrR.sf * dxR,
            vs.u,
            vs.v,
            vs.w,
            vs.weights,
            dt,
            1.0,
            ctrL.sf,
            ctrR.sf,
        )
    else
        len, n, dirc = p[3:5]
        sfL = @view ctrL.sf[:, :, :, dirc]
        sfR = @view ctrR.sf[:, :, :, dirc]

        flux_kfvs!(
            face.fw,
            face.ff,
            ctrL.f + sfL * dxL,
            ctrR.f - sfR * dxR,
            vs.u .* n[1] .+ vs.v .* n[2],
            vs.v .* n[1] .- vs.u .* n[2],
            vs.w,
            vs.weights,
            dt,
            len,
            sfL,
            sfR,
        )
        face.fw .= global_frame(face.fw, n[1], n[2])
    end

    return nothing

end

# ------------------------------------------------------------
# Low-level backends
# ------------------------------------------------------------

"""
$(SIGNATURES)

Kinetic flux vector splitting (KFVS) flux
"""
function flux_kfvs(
    fL::Y,
    fR::Y,
    u::AV{<:FN},
    dt,
    sfL = zero(fL)::Y,
    sfR = zero(fR)::Y,
) where {Y<:AV{<:FN}}

    ff = similar(fL)
    flux_kfvs!(ff, fL, fR, u, dt, sfL, sfR)

    return ff

end

"""
$(SIGNATURES)

Kinetic flux vector splitting (KFVS) flux

- @args: particle distribution functions and their left/right slopes
- @args: particle velocity quadrature points and weights
- @args: time step and cell size

1F1V for pure DOM
"""
function flux_kfvs!(
    ff::AV{<:FN},
    fL::Y,
    fR::Y,
    u::AV{<:FN},
    dt,
    sfL = zero(fL)::Y,
    sfR = zero(fR)::Y,
) where {Y<:AV{<:FN}} # 1F1V flux for pure DOM

    # upwind reconstruction
    δ = heaviside.(u)

    f = @. fL * δ + fR * (1.0 - δ)
    sf = @. sfL * δ + sfR * (1.0 - δ)

    # calculate flux
    @. ff = dt * u * f - 0.5 * dt^2 * u^2 * sf

    return nothing

end

"""
$(SIGNATURES)

1F1V
"""
function flux_kfvs!(
    fw::AV{<:FN},
    ff::AV{<:FN},
    fL::Z,
    fR::Z,
    u::A,
    ω::A,
    dt,
    sfL = zero(fL)::Z,
    sfR = zero(fR)::Z,
) where {Z<:AV{<:FN},A<:AV{<:FN}}

    # upwind reconstruction
    δ = heaviside.(u)

    f = @. fL * δ + fR * (1.0 - δ)
    sf = @. sfL * δ + sfR * (1.0 - δ)

    # calculate fluxes
    @. ff = dt * u * f - 0.5 * dt^2 * u^2 * sf
    flux_conserve!(fw, ff, u, ω)
    #fw[1] = dt * sum(ω .* u .* f) - 0.5 * dt^2 * sum(ω .* u .^ 2 .* sf)
    #fw[2] = dt * sum(ω .* u .^ 2 .* f) - 0.5 * dt^2 * sum(ω .* u .^ 3 .* sf)
    #fw[3] = dt * 0.5 * sum(ω .* u .^ 3 .* f) - 0.5 * dt^2 * 0.5 * sum(ω .* u .^ 4 .* sf)

    return nothing

end

"""
$(SIGNATURES)

Mixture
"""
function flux_kfvs!(
    fw::AM{<:FN},
    ff::AM{<:FN},
    fL::Z,
    fR::Z,
    u::A,
    ω::A,
    dt,
    sfL = zero(fL)::Z,
    sfR = zero(fR)::Z,
) where {Z<:AM{<:FN},A<:AM{<:FN}}

    for j in axes(fw, 2)
        _fw = @view fw[:, j]
        _ff = @view ff[:, j]
        _fL = @view fL[:, j]
        _fR = @view fR[:, j]
        _u = @view u[:, j]
        _ω = @view ω[:, j]
        _sfL = @view sfL[:, j]
        _sfR = @view sfR[:, j]

        flux_kfvs!(_fw, _ff, _fL, _fR, _u, _ω, dt, _sfL, _sfR)
    end

    return nothing

end

"""
$(SIGNATURES)

2F1V
"""
function flux_kfvs!(
    fw::AV{<:FN},
    fh::Y,
    fb::Y,
    hL::Z,
    bL::Z,
    hR::Z,
    bR::Z,
    u::A,
    ω::A,
    dt,
    shL = zero(hL)::Z,
    sbL = zero(bL)::Z,
    shR = zero(hR)::Z,
    sbR = zero(bR)::Z,
) where {Y<:AV{<:FN},Z<:AV{<:FN},A<:AV{<:FN}}

    # upwind reconstruction
    δ = heaviside.(u)

    h = @. hL * δ + hR * (1.0 - δ)
    b = @. bL * δ + bR * (1.0 - δ)

    sh = @. shL * δ + shR * (1.0 - δ)
    sb = @. sbL * δ + sbR * (1.0 - δ)

    # calculate fluxes
    @. fh = dt * u * h - 0.5 * dt^2 * u^2 * sh
    @. fb = dt * u * b - 0.5 * dt^2 * u^2 * sb
    flux_conserve!(fw, fh, fb, u, ω)
    #=fw[1] = dt * sum(ω .* u .* h) - 0.5 * dt^2 * sum(ω .* u .^ 2 .* sh)
    fw[2] = dt * sum(ω .* u .^ 2 .* h) - 0.5 * dt^2 * sum(ω .* u .^ 3 .* sh)
    fw[3] =
        dt * 0.5 * (sum(ω .* u .^ 3 .* h) + sum(ω .* u .* b)) -
        0.5 * dt^2 * 0.5 * (sum(ω .* u .^ 4 .* sh) + sum(ω .* u .^ 2 .* sb))=#

    return nothing

end

"""
$(SIGNATURES)

Mixture
"""
function flux_kfvs!(
    fw::AM{<:FN},
    fh::Y,
    fb::Y,
    hL::Z,
    bL::Z,
    hR::Z,
    bR::Z,
    u::A,
    ω::A,
    dt,
    shL = zero(hL)::Z,
    sbL = zero(bL)::Z,
    shR = zero(hR)::Z,
    sbR = zero(bR)::Z,
) where {Y<:AM{<:FN},Z<:AM{<:FN},A<:AM{<:FN}}

    for j in axes(fw, 2)
        _fw = @view fw[:, j]
        _fh = @view fh[:, j]
        _fb = @view fb[:, j]
        _hL = @view hL[:, j]
        _bL = @view bL[:, j]
        _hR = @view hR[:, j]
        _bR = @view bR[:, j]
        _u = @view u[:, j]
        _ω = @view ω[:, j]
        _shL = @view shL[:, j]
        _sbL = @view sbL[:, j]
        _shR = @view shR[:, j]
        _sbR = @view sbR[:, j]

        flux_kfvs!(_fw, _fh, _fb, _hL, _bL, _hR, _bR, _u, _ω, dt, _shL, _sbL, _shR, _sbR)
    end

    return nothing

end

"""
$(SIGNATURES)

3F1V @ Rykov
"""
function flux_kfvs!(
    fw::AV{<:FN},
    fh::Y,
    fb::Y,
    fr::Y,
    hL::Z,
    bL::Z,
    rL::Z,
    hR::Z,
    bR::Z,
    rR::Z,
    u::A,
    ω::A,
    dt,
    shL = zero(hL)::Z,
    sbL = zero(bL)::Z,
    srL = zero(rL)::Z,
    shR = zero(hR)::Z,
    sbR = zero(bR)::Z,
    srR = zero(rR)::Z,
) where {Y<:AV{<:FN},Z<:AV{<:FN},A<:AV{<:FN}}

    # upwind reconstruction
    δ = heaviside.(u)

    h = @. hL * δ + hR * (1.0 - δ)
    b = @. bL * δ + bR * (1.0 - δ)
    r = @. rL * δ + rR * (1.0 - δ)

    sh = @. shL * δ + shR * (1.0 - δ)
    sb = @. sbL * δ + sbR * (1.0 - δ)
    sr = @. srL * δ + srR * (1.0 - δ)

    # macro fluxes
    fw[1] = dt * sum(ω .* u .* h) - 0.5 * dt^2 * sum(ω .* u .^ 2 .* sh)
    fw[2] = dt * sum(ω .* u .^ 2 .* h) - 0.5 * dt^2 * sum(ω .* u .^ 3 .* sh)
    fw[3] =
        dt * 0.5 * (sum(ω .* u .^ 3 .* h) + sum(ω .* u .* b)) -
        0.5 * dt^2 * 0.5 * (sum(ω .* u .^ 4 .* sh) + sum(ω .* u .^ 2 .* sb))
    fw[4] = dt * 0.5 * sum(ω .* u .* r) - 0.5 * dt^2 * sum(ω .* u .^ 2 .* sr)

    # micro fluxes
    @. fh = dt * u * h - 0.5 * dt^2 * u^2 * sh
    @. fb = dt * u * b - 0.5 * dt^2 * u^2 * sb
    @. fr = dt * u * r - 0.5 * dt^2 * u^2 * sr

    return nothing

end

"""
$(SIGNATURES)

1F3V
"""
function flux_kfvs!(
    fw::AV{<:FN},
    ff::AA{<:FN,3},
    fL::Z,
    fR::Z,
    u::A,
    v::A,
    w::A,
    ω::A,
    dt::Real,
    len = 1.0::Real,
    sfL = zero(fL)::Z,
    sfR = zero(fR)::Z,
) where {Z<:AA{<:FN,3},A<:AA{<:FN,3}}

    # upwind reconstruction
    δ = heaviside.(u)

    f = @. fL * δ + fR * (1.0 - δ)
    sf = @. sfL * δ + sfR * (1.0 - δ)

    # calculate fluxes
    @. ff = (dt * u * f - 0.5 * dt^2 * u^2 * sf) * len
    flux_conserve!(fw, ff, u, v, w, ω)
    #=fw[1] = dt * sum(ω .* u .* f) - 0.5 * dt^2 * sum(ω .* u .^ 2 .* sf)
    fw[2] = dt * sum(ω .* u .^ 2 .* f) - 0.5 * dt^2 * sum(ω .* u .^ 3 .* sf)
    fw[3] = dt * sum(ω .* u .* v .* f) - 0.5 * dt^2 * sum(ω .* u .^ 2 .* v .* sf)
    fw[4] = dt * sum(ω .* u .* w .* f) - 0.5 * dt^2 * sum(ω .* u .^ 2 .* w .* sf)
    fw[5] =
        dt * 0.5 * sum(ω .* u .* (u .^ 2 .+ v .^ 2 .+ w .^ 2) .* f) -
        0.5 * dt^2 * 0.5 * sum(ω .* u .^ 2 .* (u .^ 2 .+ v .^ 2 .+ w .^ 2) .* sf)
    @. fw *= len=#

    return nothing

end

"""
$(SIGNATURES)

4F1V
"""
function flux_kfvs!(
    fw::AV{<:FN},
    fh0::Y,
    fh1::Y,
    fh2::Y,
    fh3::Y,
    h0L::Z,
    h1L::Z,
    h2L::Z,
    h3L::Z,
    h0R::Z,
    h1R::Z,
    h2R::Z,
    h3R::Z,
    u::A,
    ω::A,
    dt,
    sh0L = zero(h0L)::Z,
    sh1L = zero(h1L)::Z,
    sh2L = zero(h2L)::Z,
    sh3L = zero(h3L)::Z,
    sh0R = zero(h0R)::Z,
    sh1R = zero(h1R)::Z,
    sh2R = zero(h2R)::Z,
    sh3R = zero(h3R)::Z,
) where {Y<:AV{<:FN},Z<:AV{<:FN},A<:AV{<:FN}}

    # upwind reconstruction
    δ = heaviside.(u)

    h0 = @. h0L * δ + h0R * (1.0 - δ)
    h1 = @. h1L * δ + h1R * (1.0 - δ)
    h2 = @. h2L * δ + h2R * (1.0 - δ)
    h3 = @. h3L * δ + h3R * (1.0 - δ)

    sh0 = @. sh0L * δ + sh0R * (1.0 - δ)
    sh1 = @. sh1L * δ + sh1R * (1.0 - δ)
    sh2 = @. sh2L * δ + sh2R * (1.0 - δ)
    sh3 = @. sh3L * δ + sh3R * (1.0 - δ)

    # calculate fluxes
    fw[1] = dt * sum(ω .* u .* h0) - 0.5 * dt^2 * sum(ω .* u .^ 2 .* sh0)
    fw[2] = dt * sum(ω .* u .^ 2 .* h0) - 0.5 * dt^2 * sum(ω .* u .^ 3 .* sh0)
    fw[3] = dt * sum(ω .* u .* h1) - 0.5 * dt^2 * sum(ω .* u .^ 2 .* sh1)
    fw[4] = dt * sum(ω .* u .* h2) - 0.5 * dt^2 * sum(ω .* u .^ 2 .* sh2)
    fw[5] =
        dt * 0.5 * (sum(ω .* u .^ 3 .* h0) + sum(ω .* u .* h3)) -
        0.5 * dt^2 * 0.5 * (sum(ω .* u .^ 4 .* sh0) + sum(ω .* u .^ 2 .* sh3))

    @. fh0 = dt * u * h0 - 0.5 * dt^2 * u^2 * sh0
    @. fh1 = dt * u * h1 - 0.5 * dt^2 * u^2 * sh1
    @. fh2 = dt * u * h2 - 0.5 * dt^2 * u^2 * sh2
    @. fh3 = dt * u * h3 - 0.5 * dt^2 * u^2 * sh3

    return nothing

end

"""
$(SIGNATURES)

Mixture
"""
function flux_kfvs!(
    fw::AM{<:FN},
    fh0::Y,
    fh1::Y,
    fh2::Y,
    fh3::Y,
    h0L::Z,
    h1L::Z,
    h2L::Z,
    h3L::Z,
    h0R::Z,
    h1R::Z,
    h2R::Z,
    h3R::Z,
    u::A,
    ω::A,
    dt,
    sh0L = zero(h0L)::Z,
    sh1L = zero(h1L)::Z,
    sh2L = zero(h2L)::Z,
    sh3L = zero(h3L)::Z,
    sh0R = zero(h0R)::Z,
    sh1R = zero(h1R)::Z,
    sh2R = zero(h2R)::Z,
    sh3R = zero(h3R)::Z,
) where {Y<:AM{<:FN},Z<:AM{<:FN},A<:AM{<:FN}}

    for j in axes(fw, 2)
        _fw = @view fw[:, j]
        _fh0 = @view fh0[:, j]
        _fh1 = @view fh1[:, j]
        _fh2 = @view fh2[:, j]
        _fh3 = @view fh3[:, j]

        flux_kfvs!(
            _fw,
            _fh0,
            _fh1,
            _fh2,
            _fh3,
            h0L[:, j],
            h1L[:, j],
            h2L[:, j],
            h3L[:, j],
            h0R[:, j],
            h1R[:, j],
            h2R[:, j],
            h3R[:, j],
            u[:, j],
            ω[:, j],
            dt,
            sh0L[:, j],
            sh1L[:, j],
            sh2L[:, j],
            sh3L[:, j],
            sh0R[:, j],
            sh1R[:, j],
            sh2R[:, j],
            sh3R[:, j],
        )
    end

    return nothing

end

"""
$(SIGNATURES)

1F2V
"""
function flux_kfvs!(
    fw::AV{<:FN},
    ff::Union{AV{<:FN},AM{<:FN}},
    fL::Z,
    fR::Z,
    u::A,
    v::A,
    ω::A,
    dt,
    len,
    sfL = zero(fL)::Z,
    sfR = zero(fR)::Z,
) where {Z<:Union{AV{<:FN},AM{<:FN}},A<:Union{AV{<:FN},AM{<:FN}}}

    #--- upwind reconstruction ---#
    δ = heaviside.(u)

    f = @. fL * δ + fR * (1.0 - δ)
    sf = @. sfL * δ + sfR * (1.0 - δ)

    #--- calculate fluxes ---#
    @. ff = (dt * u * f - 0.5 * dt^2 * u^2 * sf) * len
    flux_conserve!(fw, ff, u, v, ω)
    #=fw[1] = dt * sum(ω .* u .* f) - 0.5 * dt^2 * sum(ω .* u .^ 2 .* sf)
    fw[2] = dt * sum(ω .* u .^ 2 .* f) - 0.5 * dt^2 * sum(ω .* u .^ 3 .* sf)
    fw[3] = dt * sum(ω .* v .* u .* f) - 0.5 * dt^2 * sum(ω .* v .* u .^ 2 .* sf)
    fw[4] =
        dt * 0.5 * sum(ω .* u .* (u .^ 2 .+ v .^ 2) .* f) -
        0.5 * dt^2 * 0.5 * sum(ω .* u .^ 2 .* (u .^ 2 .+ v .^ 2) .* sf)
    fw .*= len=#

    return nothing

end

"""
$(SIGNATURES)

2F2V
"""
function flux_kfvs!(
    fw::AV{<:FN},
    fh::Y,
    fb::Y,
    hL::Z,
    bL::Z,
    hR::Z,
    bR::Z,
    u::A,
    v::A,
    ω::A,
    dt,
    len,
    shL = zero(hL)::Z,
    sbL = zero(bL)::Z,
    shR = zero(hR)::Z,
    sbR = zero(bR)::Z,
) where {
    Y<:Union{AV{<:FN},AM{<:FN}},
    Z<:Union{AV{<:FN},AM{<:FN}},
    A<:Union{AV{<:FN},AM{<:FN}},
}

    #--- upwind reconstruction ---#
    δ = heaviside.(u)

    h = @. hL * δ + hR * (1.0 - δ)
    b = @. bL * δ + bR * (1.0 - δ)
    sh = @. shL * δ + shR * (1.0 - δ)
    sb = @. sbL * δ + sbR * (1.0 - δ)

    #--- calculate fluxes ---#
    @. fh = (dt * u * h - 0.5 * dt^2 * u^2 * sh) * len
    @. fb = (dt * u * b - 0.5 * dt^2 * u^2 * sb) * len
    flux_conserve!(fw, fh, fb, u, v, ω)
    #=fw[1] = dt * sum(ω .* u .* h) - 0.5 * dt^2 * sum(ω .* u .^ 2 .* sh)
    fw[2] = dt * sum(ω .* u .^ 2 .* h) - 0.5 * dt^2 * sum(ω .* u .^ 3 .* sh)
    fw[3] = dt * sum(ω .* v .* u .* h) - 0.5 * dt^2 * sum(ω .* v .* u .^ 2 .* sh)
    fw[4] =
        dt * 0.5 * (sum(ω .* u .* (u .^ 2 .+ v .^ 2) .* h) + sum(ω .* u .* b)) -
        0.5 *
        dt^2 *
        0.5 *
        (sum(ω .* u .^ 2 .* (u .^ 2 .+ v .^ 2) .* sh) + sum(ω .* u .^ 2 .* sb))
    fw .*= len=#

    return nothing

end

"""
$(SIGNATURES)

3F2V
"""
function flux_kfvs!(
    fw::AV{<:FN},
    fh0::Y,
    fh1::Y,
    fh2::Y,
    h0L::Z,
    h1L::Z,
    h2L::Z,
    h0R::Z,
    h1R::Z,
    h2R::Z,
    u::A,
    v::A,
    ω::A,
    dt,
    len,
    sh0L = zero(h0L)::Z,
    sh1L = zero(h1L)::Z,
    sh2L = zero(h2L)::Z,
    sh0R = zero(h0R)::Z,
    sh1R = zero(h1R)::Z,
    sh2R = zero(h2R)::Z,
) where {Y<:AM{<:FN},Z<:AM{<:FN},A<:AM{<:FN}}

    #--- upwind reconstruction ---#
    δ = heaviside.(u)

    h0 = @. h0L * δ + h0R * (1.0 - δ)
    h1 = @. h1L * δ + h1R * (1.0 - δ)
    h2 = @. h2L * δ + h2R * (1.0 - δ)
    sh0 = @. sh0L * δ + sh0R * (1.0 - δ)
    sh1 = @. sh1L * δ + sh1R * (1.0 - δ)
    sh2 = @. sh2L * δ + sh2R * (1.0 - δ)

    #--- calculate fluxes ---#
    fw[1] = dt * sum(ω .* u .* h0) - 0.5 * dt^2 * sum(ω .* u .^ 2 .* sh0)
    fw[2] = dt * sum(ω .* u .^ 2 .* h0) - 0.5 * dt^2 * sum(ω .* u .^ 3 .* sh0)
    fw[3] = dt * sum(ω .* u .* v .* h0) - 0.5 * dt^2 * sum(ω .* u .^ 2 .* v .* sh0)
    fw[4] = dt * sum(ω .* u .* h1) - 0.5 * dt^2 * sum(ω .* u .^ 2 .* sh1)
    fw[5] =
        dt * 0.5 * (sum(ω .* u .* (u .^ 2 .+ v .^ 2) .* h0) + sum(ω .* u .* h2)) -
        0.5 *
        dt^2 *
        0.5 *
        (sum(ω .* u .^ 2 .* (u .^ 2 .+ v .^ 2) .* sh0) + sum(ω .* u .^ 2 .* sh2))

    fw .*= len
    @. fh0 = (dt * u * h0 - 0.5 * dt^2 * u^2 * sh0) * len
    @. fh1 = (dt * u * h1 - 0.5 * dt^2 * u^2 * sh1) * len
    @. fh2 = (dt * u * h2 - 0.5 * dt^2 * u^2 * sh2) * len

    return nothing

end

"""
$(SIGNATURES)

Mixture
"""
function flux_kfvs!(
    fw::AM{<:FN},
    fh0::Y,
    fh1::Y,
    fh2::Y,
    h0L::Z,
    h1L::Z,
    h2L::Z,
    h0R::Z,
    h1R::Z,
    h2R::Z,
    u::A,
    v::A,
    ω::A,
    dt,
    len,
    sh0L = zero(h0L)::Z,
    sh1L = zero(h1L)::Z,
    sh2L = zero(h2L)::Z,
    sh0R = zero(h0R)::Z,
    sh1R = zero(h1R)::Z,
    sh2R = zero(h2R)::Z,
) where {Y<:AA{<:FN,3},Z<:AA{<:FN,3},A<:AA{<:FN,3}}

    #--- reconstruct initial distribution ---#
    δ = heaviside.(u)

    h0 = @. h0L * δ + h0R * (1.0 - δ)
    h1 = @. h1L * δ + h1R * (1.0 - δ)
    h2 = @. h2L * δ + h2R * (1.0 - δ)

    sh0 = @. sh0L * δ + sh0R * (1.0 - δ)
    sh1 = @. sh1L * δ + sh1R * (1.0 - δ)
    sh2 = @. sh2L * δ + sh2R * (1.0 - δ)

    for j = 1:2
        fw[1, j] =
            dt * sum(ω[:, :, j] .* u[:, :, j] .* h0[:, :, j]) -
            0.5 * dt^2 * sum(ω[:, :, j] .* u[:, :, j] .^ 2 .* sh0[:, :, j])
        fw[2, j] =
            dt * sum(ω[:, :, j] .* u[:, :, j] .^ 2 .* h0[:, :, j]) -
            0.5 * dt^2 * sum(ω[:, :, j] .* u[:, :, j] .^ 3 .* sh0[:, :, j])
        fw[3, j] =
            dt * sum(ω[:, :, j] .* v[:, :, j] .* u[:, :, j] .* h0[:, :, j]) -
            0.5 * dt^2 * sum(ω[:, :, j] .* u[:, :, j] .^ 2 .* v[:, :, j] .* sh0[:, :, j])
        fw[4, j] =
            dt * sum(ω[:, :, j] .* u[:, :, j] .* h1[:, :, j]) -
            0.5 * dt^2 * sum(ω[:, :, j] .* u[:, :, j] .^ 2 .* sh1[:, :, j])
        fw[5, j] =
            dt *
            0.5 *
            (
                sum(
                    ω[:, :, j] .* u[:, :, j] .* (u[:, :, j] .^ 2 .+ v[:, :, j] .^ 2) .*
                    h0[:, :, j],
                ) + sum(ω[:, :, j] .* u[:, :, j] .* h2[:, :, j])
            ) -
            0.5 *
            dt^2 *
            0.5 *
            (
                sum(
                    ω[:, :, j] .* u[:, :, j] .^ 2 .* (u[:, :, j] .^ 2 .+ v[:, :, j] .^ 2) .*
                    sh0[:, :, j],
                ) + sum(ω[:, :, j] .* u[:, :, j] .^ 2 .* sh2[:, :, j])
            )

        @. fh0[:, :, j] =
            dt * u[:, :, j] * h0[:, :, j] - 0.5 * dt^2 * u[:, :, j]^2 * sh0[:, :, j]
        @. fh1[:, :, j] =
            dt * u[:, :, j] * h1[:, :, j] - 0.5 * dt^2 * u[:, :, j]^2 * sh1[:, :, j]
        @. fh2[:, :, j] =
            dt * u[:, :, j] * h2[:, :, j] - 0.5 * dt^2 * u[:, :, j]^2 * sh2[:, :, j]
    end

    @. fw *= len
    @. fh0 *= len
    @. fh1 *= len
    @. fh2 *= len

    return nothing

end


#
# This file is a part of MolecularGraph.jl
# Licensed under the MIT License http://opensource.org/licenses/MIT
#

export
    mcismol, mcismolsize,
    mcesmol, mcesmolsize

"""
    mcismol(mol1::UndirectedGraph, mol2::UndirectedGraph; kwargs...
        ) -> Tuple{Dict{Int,Int},Symbol}

Compute maximum common induced substructure (MCIS) of mol1 and mol2.

## Keyword arguments

- connected(Bool): if true, apply connected MCS constraint.
- topological(Bool): if true, apply topological constraint.
- diameter(Int): distance cutoff for topological constraint.
- tolerance(Int): distance mismatch tolerance for topological constraint.
- timeout(Int): abort calculation and return suboptimal results if the execution
time has reached the given value (default=60, in seconds).
- targetsize(Int): abort calculation and return suboptimal result so far if the
given mcs size achieved.

# References

1. Kawabata, T. (2011). Build-Up Algorithm for Atomic Correspondence between
Chemical Structures. Journal of Chemical Information and Modeling, 51(8),
1775–1787. https://doi.org/10.1021/ci2001023
1. https://www.jstage.jst.go.jp/article/ciqs/2017/0/2017_P4/_article/-char/en
"""
function mcismol(mol1::UndirectedGraph, mol2::UndirectedGraph; kwargs...)
    afunc = atommatch(mol1, mol2)
    bfunc = bondmatch(mol1, mol2)
    return findmcis(
        mol1, mol2, nodematcher=afunc, edgematcher=bfunc; kwargs...)
end

mcismolsize(mol1, mol2; kwargs...) = length(mcismol(mol1, mol2; kwargs...)[1])

"""
    mcesmol(mol1::UndirectedGraph, mol2::UndirectedGraph; kwargs...
        ) -> Tuple{Dict{Int,Int},Symbol}

Compute maximum common edge induced substructure (MCES) of mol1 and mol2.
"""
function mcesmol(mol1::UndirectedGraph, mol2::UndirectedGraph; kwargs...)
    afunc = atommatch(mol1, mol2)
    bfunc = bondmatch(mol1, mol2)
    return findmces(
        mol1, mol2, nodematcher=afunc, edgematcher=bfunc; kwargs...)
end

mcesmolsize(mol1, mol2; kwargs...) = length(mcesmol(mol1, mol2; kwargs...)[1])


"""
    mol_helmholtz(model,state::ThermodynamicState,unit,[options...])::Real

Keyword symbols: `:mol_a`, `:a`

Compute the molar helmholtz energy (J/mol), from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function mol_helmholtz end

"""
    mass_helmholtz(model,state::ThermodynamicState,unit,[options...])::Real

Keyword symbols: `:mass_a`

Compute the specific (mass) helmholtz energy (J/kg), from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function mass_helmholtz end

"""
    total_helmholtz(model,state::ThermodynamicState,unit,[options...])::Real

Keyword symbols: `:total_a`

Compute the total helmholtz energy (J), from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function total_helmholtz end

#gibbs energy

"""
    mol_gibbs(model,state::ThermodynamicState,unit,[options...])::Real

Keyword symbols: `:mol_g`, `:g`

Compute the molar gibbs energy (J/mol), from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function mol_gibbs end

"""
    mass_gibbs(model,state::ThermodynamicState,unit,[options...])::Real

Keyword symbols: `:mass_g`

Compute the specific (mass) gibbs energy (J/kg), from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function mass_gibbs end

"""
    total_gibbs(model,state::ThermodynamicState,unit,[options...])::Real

Keyword symbols: `:total_g`

Compute the total gibbs energy (J), from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function total_gibbs end

#enthalpy

"""
    mol_enthalpy(model,state::ThermodynamicState,unit,[options...])::Real

Keyword symbols: `:mol_h`, `:h`

Compute the molar enthalpy (J/mol), from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function mol_enthalpy end

"""
    mass_enthalpy(model,state::ThermodynamicState,unit,[options...])::Real

Keyword symbols: `:mass_h`

Compute the specific (mass) enthalpy (J/kg), from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function mass_enthalpy end

"""
    total_enthalpy(model,state::ThermodynamicState,unit,[options...])::Real

Keyword symbols: `:total_h`

Compute the total enthalpy (J), from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function total_enthalpy end

#internal energy

"""
    mol_internal_energy(model,state::ThermodynamicState,unit,[options...])::Real

Keyword symbols: `:mol_u`, `:u`

Compute the molar internal energy (J/mol), from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function mol_internal_energy end

"""
    mass_internal_energy(model,state::ThermodynamicState,unit,[options...])::Real

Keyword symbols: `:mass_u`

Compute the specific (mass) internal energy (J/kg), from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function mass_internal_energy end

"""
    total_internal_energy(model,state::ThermodynamicState,unit,[options...])::Real

Keyword symbols: `:total_u`

Compute the total internal energy (J), from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function total_internal_energy end

#entropy

"""
    mol_entropy(model,state::ThermodynamicState,unit,[options...])::Real

Keyword symbols: `:mol_s`, `:s`

Compute the molar entropy (J/(mol K)), from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function mol_entropy end

"""
    mass_entropy(model,state::ThermodynamicState,unit,[options...])::Real

Keyword symbols: `:mass_s`

Compute the specific (mass) entropy (J/(kg K)), from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function mass_entropy end

"""
    total_entropy(model,state::ThermodynamicState,unit,[options...])::Real

Keyword symbols: `:total_s`

Compute the total entropy (J/K), from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function total_entropy end

#temperature an pressure
"""
    temperature(model,state::ThermodynamicState,unit,[options...])::Real

Keyword symbols: `:t`, `:T`

Compute the temperature (K), from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function temperature end

"""
pressure(model,state::ThermodynamicState,unit,[options...])::Real

    Keyword symbols: `:p`, `:P`

Compute the pressure (Pa), from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function pressure end

#volume amounts

"""
    mol_volume(model,state::ThermodynamicState,unit,[options...])::Real

Keyword symbols: `:mol_v`, `:v`

Compute the molar volume (m³/mol), from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function mol_volume end

"""
    mass_volume(model,state::ThermodynamicState,unit,[options...])::Real

Keyword symbols: `:mass_v`

Compute the specific (mass) volume (m³/kg), from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function mass_volume end

"""
    total_volume(model,state::ThermodynamicState,unit,[options...])::Real

Keyword symbols: `:total_v`, `:V`

Compute the total volume (m³), from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function total_volume end

"""
    mol_density(model,state::ThermodynamicState,unit,[options...])::Real

Keyword symbols: `:mol_rho`, `:rho`, `:mol_ρ`, `:ρ`

Compute the molar density (mol/m³), from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function mol_density end

"""
    mass_density(model,state::ThermodynamicState,unit,[options...])::Real

Keyword symbols: `:mass_rho`, `:mass_ρ`

Compute the specific (mass) density (kg/m³), from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function mass_density end

"""
    mol_fraction(model,state::ThermodynamicState,unit,[options...])::AbstractVector

Keyword symbols: `:xn`

Compute the molar fraction of each present compound present in the provided system, from or using the specifications as base.

The unit argument is not used here. but its present for consistency.
"""
function mol_fraction end

"""
    mass_fraction(model,state::ThermodynamicState,unit,[options...])::AbstractVector

Keyword symbols: `:xm`

Compute the mass fraction of each present compound present in the provided system, from or using the specifications as base.

The unit argument is not used here. but its present for consistency.
"""
function mass_fraction end

"""
    mol_number(model,state::ThermodynamicState,unit,[options...])::AbstractVector

Keyword symbols: `:n`

Compute the amount of moles (in mol) of each present compound present in the provided system, from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function mol_number end

"""
    mass_number(model,state::ThermodynamicState,unit,[options...])::AbstractVector

Keyword symbols: `:m`

Compute the amount of mass (in kg) of each present compound present in the provided system, from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function mass_number end

"""
    moles(model,state::ThermodynamicState,unit,[options...])::Real

Keyword symbols: `:moles`

Compute the total amount of moles (in mol) of the provided system, from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function moles end

"""
    mass(model,state::ThermodynamicState,unit,[options...])::Real

Keyword symbols: `:mass`

Compute the total amount of mass (in kg) of the provided system, from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function mass end

"""
    molar_mass(model,state::ThermodynamicState,unit,[options...])::Real

Compute the amount of mass per unit of moles (in kg/mol) of the provided state, from or using the specifications as base.

If you need unitful units, use the macro `@to_units` before the function call

"""
function molar_mass end

"""
    options(model,state::ThermodynamicState)::NamedTuple

Keyword symbols: `:options`

Returns any options related to the model, stored in the state or the model.

Returns an empty named tuple otherwise.

"""
function options end

"""
    phase(model,state::ThermodynamicState)::NamedTuple

Keyword symbols: `:phase`

Returns a symbol indicating the phase specified in the state or the model.

Returns `:unspecified` otherwise.

"""
function phase end

"""
    quality(model,state::ThermodynamicState)::NamedTuple

Keyword symbols: `:quality`

Returns the vapor quality stored in the state

Returns NaN otherwise.

This function is an alias for `mass_vapor_fraction`.

"""
function quality end

"""
    molecular_weight(model)

Keyword symbols: `:mw`

Returns the molecular weight stored in a model

Returns `nothing` if not defined.

it should return a list of the molecular weights. in g/mol. You can return a single number if the model has only one component.

"""
function molecular_weight end


module ContractionSequenceOptimization

export optimal_contraction_sequence, contraction_cost

include("utils.jl")
include("three_tensors.jl")
include("breadth_first_constructive.jl")
include("contraction_cost.jl")

#
# The integer type of the dimensions and costs.
# Needs to be large to avoid overflow, but larger
# integer types are a little bit slower.
#
# For very large dimensions, one could try:
#
# https://github.com/rfourquet/BitIntegers.jl
#
# however in that limit it would be best to use
# symbolic dimensions.
#
const DimT = UInt128

"""
    optimal_contraction_sequence(T)

Returns a contraction sequence for contracting the tensors `T`. The sequence is generally optimal (currently, outer product contractions are skipped, but some optimal sequences require outer product contractions).
"""
function optimal_contraction_sequence(T)
  if length(T) == 1
    return Any[1]
  elseif length(T) == 2
    return Any[1, 2]
  elseif length(T) == 3
    return optimal_contraction_sequence(T[1], T[2], T[3])
  end
  return breadth_first_constructive(T)
end

end # module ContractionSequenceOptimization


# abstract ParameterUpdater
# abstract ParameterUpdaterState

# # ----------------------------------------

"Allows for global storage of a ParameterUpdater, so that you don't need to pass it around."
type CurrentUpdater
  updater::ParameterUpdater
end

const _current_updater = CurrentUpdater(AdaMaxUpdater())

"Get the current global gradient updater used for gradient updates."
current_updater() = _current_updater.updater

"Set the current global gradient updater used for gradient updates."
current_updater!(updater::ParameterUpdater) = (_current_updater.updater = updater)

# # ----------------------------------------

"Allows for global storage of a ModelLoss, so that you don't need to pass it around."
type CurrentModelLoss
  mloss::ModelLoss
end

const _current_mloss = CurrentModelLoss(L2DistLoss())

"Get the current global gradient mloss used for gradient updates."
current_mloss() = _current_mloss.mloss

"Set the current global gradient mloss used for gradient updates."
current_mloss!(mloss::ModelLoss) = (_current_mloss.mloss = mloss)

# # ----------------------------------------

"Allows for global storage of a ParameterLoss, so that you don't need to pass it around."
type CurrentParameterLoss
  ploss::ParameterLoss
end

const _current_ploss = CurrentParameterLoss(NoParameterLoss())

"Get the current global gradient ploss used for gradient updates."
current_ploss() = _current_ploss.ploss

"Set the current global gradient ploss used for gradient updates."
current_ploss!(ploss::ParameterLoss) = (_current_ploss.ploss = ploss)

# --------------------------------------------------------------

"Enacts a strategy to adjust the learning rate"
abstract LearningRateModel

immutable FixedLearningRate <: LearningRateModel end
OnlineStats.fit!(lrmodel::FixedLearningRate, err::Float64) = nothing

"Adapts learning rate based on relative variance of the changes in the test error"
immutable AdaptiveLearningRate <: LearningRateModel
  updater::ParameterUpdater
  errordiff::Diff
  diffvar::Variance
  adjustmentPct::Float64
  cutoffRatio::Float64
end

function AdaptiveLearningRate(updater::ParameterUpdater,
                              adjustmentPct = 1e-2,
                              cutoffRatio = 1e-1;
                              wgt = ExponentialWeight(20))
  AdaptiveLearningRate(updater, Diff(), Variance(wgt), adjustmentPct, cutoffRatio)
end

# if the error is decreasing at a large rate relative to the variance, increase the learning rate (speed it up)
function OnlineStats.fit!(lrmodel::AdaptiveLearningRate, err::Float64)
  fit!(lrmodel.errordiff, err)
  fit!(lrmodel.diffvar, diff(lrmodel.errordiff))
  m = mean(lrmodel.diffvar)
  s = std(lrmodel.diffvar)
  if s > 0.0
    pct = lrmodel.adjustmentPct * (m / s < -lrmodel.cutoffRatio ? 1.0 : -1.0)
    lrmodel.updater.η *= (1.0 + pct)
  end
end


module Ntl

include("src/utils.jl")
include("src/models.jl")
include("src/samplers.jl")
include("src/fit.jl")
include("src/evaluate.jl")
include("src/plot.jl")
include("src/synthetic.jl")

end


using NoteSequences.PerformanceRepr

const pe = PerformanceEvent

@testset "PerformanceEvent encoding/decoding" begin
    perfencoder = PerformanceOneHotEncoding(num_velocitybins=16)
    pairs = [
        (pe(NOTE_ON, 60), 61),
        (pe(NOTE_ON, 0), 1),
        (pe(NOTE_ON, 22), 23),
        (pe(NOTE_ON, 127), 128),
        (pe(NOTE_OFF, 72), 201),
        (pe(NOTE_OFF, 0), 129),
        (pe(NOTE_OFF, 22), 151),
        (pe(NOTE_OFF, 127), 256),
        (pe(TIME_SHIFT, 10), 266),
        (pe(TIME_SHIFT, 1), 257),
        (pe(TIME_SHIFT, 72), 328),
        (pe(TIME_SHIFT, 100), 356),
        (pe(VELOCITY, 5), 361),
        (pe(VELOCITY, 1), 357),
        (pe(VELOCITY, 16), 372)
    ]

    for (event, index) in pairs
        @test index == encode_event(event, perfencoder)
        @test event == decode_event(index, perfencoder)
    end
end

@testset "Performance steps and setlength!" begin
    @testset "Add length" begin
        perf = Performance(100)
        setlength!(perf, 42)
        @test perf.numsteps == 42
        @test perf.events == [pe(TIME_SHIFT, 42)]

        setlength!(perf, 142)
        @test perf.numsteps == 142
        @test perf.events == [pe(TIME_SHIFT, 100), pe(TIME_SHIFT, 42)]

        setlength!(perf, 200)
        @test perf.numsteps == 200
        @test perf.events == [pe(TIME_SHIFT, 100), pe(TIME_SHIFT, 100)]
    end

    @testset "Remove length" begin
        perf = Performance(100)
        events = [
            pe(NOTE_ON, 60),
            pe(TIME_SHIFT, 100),
            pe(NOTE_OFF, 60),
            pe(NOTE_ON, 64),
            pe(TIME_SHIFT, 100),
            pe(NOTE_OFF, 64),
            pe(NOTE_ON, 67),
            pe(TIME_SHIFT, 100),
            pe(NOTE_OFF, 67)
        ]
        append!(perf, events)
        setlength!(perf, 200)
        result_events = [
            pe(NOTE_ON, 60),
            pe(TIME_SHIFT, 100),
            pe(NOTE_OFF, 60),
            pe(NOTE_ON, 64),
            pe(TIME_SHIFT, 100),
            pe(NOTE_OFF, 64),
            pe(NOTE_ON, 67)
        ]

        @test perf.events == result_events

        setlength!(perf, 50)
        result_events = [
            pe(NOTE_ON, 60),
            pe(TIME_SHIFT, 50)
        ]

        @test perf.events == result_events
    end

    @testset "numsteps" begin
        perf = Performance(100)
        events = [
            pe(VELOCITY, 32),
            pe(NOTE_ON, 60),
            pe(TIME_SHIFT, 100),
            pe(NOTE_OFF, 60)
        ]
        append!(perf, events)

        @test perf.numsteps == 100
    end
end