module ProcessInputFile

using TOML
using Random
using Statistics
using LinearAlgebra
using Logging
using LibGit2
using Serialization
using Parameters
using Pkg

using ..UnitCells: UnitCell
using ..Lattices: Lattice
using ..Models: HolsteinModel, SSHModel, AbstractModel
using ..Models: assign_μ!, assign_ω!, assign_λ!, assign_λ₂!, assign_ω₄!
using ..Models: assign_t!, assign_ωᵢⱼ!, assign_hopping!
using ..Models: assign_datafolder!
using ..Models: initialize_model!, update_model!, read_phonons!, mulM!, mulMᵀ!
using ..MuFinder: MuTuner, update_μ!
using ..GreensFunctions: EstimateGreensFunction, update!
using ..InitializePhonons: init_phonons_half_filled!
using ..LangevinDynamics: EulerDynamics, RungeKuttaDynamics, HeunsDynamics
using ..HMC: HybridMonteCarlo
using ..SpecialUpdates: SpecialUpdate, NullUpdate, ReflectionUpdate, SwapUpdate
using ..FourierAcceleration: FourierAccelerator, update_Q!, update_M!
using ..SimulationParams: SimulationParameters
using ..Measurements: initialize_measurements_container, initialize_measurement_folders!
using ..KPMPreconditioners: LeftRightKPMPreconditioner, SymmetricKPMPreconditioner

export process_input_file, initialize_holstein_model

function process_input_file(filename::String,input::Dict)

    # Input file must describe either a Langevin or HMC Simulation but not both.
    @assert haskey(input,"hmc") ⊻ haskey(input,"langevin")

    # Input file must describe either a Holstein or SSH model but not both.
    @assert haskey(input,"holstein") ⊻ haskey(input,"ssh")

    ##################################
    ## DEFINE SIMULATION PARAMETERS ##
    ##################################

    # initialize simulation parameters
    sim_params = initialize_simulation_params(input)

    # copy input file into data folder
    cp(filename, joinpath(sim_params.datafolder,filename))

    ######################
    ## INITIALIZE MODEL ##
    ######################

    model = initialize_model(input)

    ##############################
    ## INITIALIZE PHONON FIELDS ##
    ##############################

    initialize_phonon_fields!(input,model)

    #####################################
    ## TUNE DENSITY/CHEMICAL POTENTIAL ##
    #####################################

    # initialize MuTuner
    μ_tuner = initialize_mutuner(input,model)

    ###########################
    ## DEFINE PRECONDITIONER ##
    ###########################

    preconditioner = initialize_preconditioner(input,model)

    #################################
    ## DEFINE FOURIER ACCELERATION ##
    #################################

    # initialize FourierAccelerator
    fa = initialize_fourieraccelerator(input, model)

    #####################
    ## DEFINE DYNAMICS ##
    #####################

    burnin_dynamics, simulation_dynamics = initialize_dynamics(input,model)

    ############################
    ## DEFINE SPECIAL UDPATES ##
    ############################

    burnin_reflect_update, sim_reflect_update = initialize_reflect_update(input,model,burnin_dynamics,simulation_dynamics)
    burnin_swap_update,    sim_swap_update    = initialize_swap_update(input,model,burnin_dynamics,simulation_dynamics)

    #########################
    ## DEFINE MEASUREMENTS ##
    #########################

    # construct object of estimating Green's function
    Gr = EstimateGreensFunction(model,input["measurements"]["num_random_vectors"])

    # construct measurements container
    container = initialize_measurements_container(model,input["measurements"],sim_params.datafolder)

    # initialize measurement files
    initialize_measurement_folders!(container)

    #################################
    ## INITIALIZE SIMULATION STATS ##
    #################################

    sim_stats = initialize_sim_stats()

    return (model, Gr, μ_tuner, sim_params, simulation_dynamics, burnin_dynamics,
            burnin_reflect_update, sim_reflect_update,
            burnin_swap_update, sim_swap_update,
            fa, preconditioner, container, 1, 1, sim_stats)
end

function process_checkpoint(input::Dict)

    ############################
    ## DESERIALIZE CHECKPOINT ##
    ############################

    chkpnt = deserialize(joinpath(input["simulation"]["datafolder"],"checkpoint.jls"))

    #######################
    ## UNPACK CHECKPOINT ##
    #######################

    @unpack model, μ_tuner, container, burnin_start, sim_start, sim_stats = chkpnt

    ######################################
    ## INITIALIZE SIMULATION PARAMETERS ##
    ######################################

    sim_params = initialize_simulation_params(input)

    #########################
    ## INITIALIZE DYNAMICS ##
    #########################

    burnin_dynamics, simulation_dynamics = initialize_dynamics(input,model,burnin_start,sim_start)

    ###########################
    ## DEFINE SPECIAL UDPATE ##
    ###########################

    burnin_reflect_update, sim_reflect_update = initialize_reflect_update(input,model,burnin_dynamics,simulation_dynamics)
    burnin_swap_update,    sim_swap_update    = initialize_swap_update(input,model,burnin_dynamics,simulation_dynamics)

    ###############################
    ## INITIALIZE PRECONDITIONER ##
    ###############################

    preconditioner = initialize_preconditioner(input,model)

    ##########################################
    ## INITIALIZE GREENS FUNCTION ESTIMATOR ##
    ##########################################

    Gr = EstimateGreensFunction(model,input["measurements"]["num_random_vectors"])

    ###################################
    ## INITIALIZE FOUIER ACCELERATOR ##
    ###################################

    fa = initialize_fourieraccelerator(input, model)

    return (model, Gr, μ_tuner, sim_params, simulation_dynamics, burnin_dynamics,
            burnin_reflect_update, sim_reflect_update,
            burnin_swap_update, sim_swap_update,
            fa, preconditioner, container, burnin_start, sim_start, sim_stats)
end

###########################################################
## METHODS INITIALIZING SIMULATION THAT ARE NOT EXPORTED ##
###########################################################

"""
Initialize a hamiltonian.
"""
function initialize_model(input::Dict)

    if haskey(input,"holstein") && haskey(input,"ssh")
        error("Config file cannot include both ssh and holstein tables.")
    end

    if !haskey(input,"holstein") && !haskey(input,"ssh")
        error("No valid model table detected in config file.")
    end

    # initialize random number generator
    rng = initialize_rng(input)

    if haskey(input,"holstein")
        model = initialize_holstein_model(input,rng)
    elseif haskey(input,"ssh")
        model = initialize_ssh_model(input,rng)
    else
        error("Neither holstein or ssh model defined.")
    end

    # assign data folder to model
    assign_datafolder!(model,input["simulation"]["datafolder"])

    return model
end

"""
Initialize Holstein Model from config file.
"""
function initialize_holstein_model(input::Dict,rng::AbstractRNG)

    # define lattice geometry
    unit_cell = UnitCell(input["lattice"]["ndim"],
                         input["lattice"]["norbits"],
                         hcat(input["lattice"]["lattice_vectors"]...),
                         hcat(input["lattice"]["basis_vectors"]...))

    # define lattice
    lattice = Lattice(unit_cell, input["lattice"]["L"])

    # restart for GMRES solver
    if haskey(input["solver"],"restart")
        restart = input["solver"]["restart"]
    else
        restart = 20
    end

    # initialize holstein model
    holstein = HolsteinModel(lattice,
                             input["holstein"]["beta"],
                             input["holstein"]["dtau"],
                             is_complex      = false,
                             iterativesolver = input["solver"]["type"],
                             rng             = rng,
                             tol             = input["solver"]["tol"],
                             maxiter         = input["solver"]["maxiter"],
                             restart         = restart)

    # adding phonon frequencies
    if haskey(input["holstein"],"omega")
        for d in input["holstein"]["omega"]
            stddev = 0.0
            if haskey(d,"stddev")
                stddev = d["stddev"]
            end
            for orbit in d["orbit"]
                assign_ω!(holstein,d["val"],stddev,orbit)
            end
        end
    end

    # adding chemical potential
    if haskey(input["holstein"],"mu")
        for d in input["holstein"]["mu"]
            stddev = 0.0
            if haskey(d,"stddev")
                stddev = d["stddev"]
            end
            for orbit in d["orbit"]
                assign_μ!(holstein,d["val"],stddev,orbit)
            end
        end
    end

    # check in anharmic term defined
    if haskey(input["holstein"],"omega4")
        # adding anharmoic term to holstein model
        for d in input["holstein"]["omega4"]
            stddev = 0.0
            if haskey(d,"stddev")
                stddev = d["stddev"]
            end
            for orbit in d["orbit"]
                assign_ω₄!(holstein,d["val"],stddev,orbit)
            end
        end
    end

    # check if any hopping defined
    if haskey(input["holstein"],"t")
        for t in input["holstein"]["t"]
            stddev = 0.0
            if haskey(t,"stddev")
                stddev = t["stddev"]
            end
            assign_t!(holstein, t["val"], stddev, t["orbit"][1], t["orbit"][2], Vector{Int}(t["dL"]))
        end
    end

    # adding electron-phonon coupling
    if haskey(input["holstein"],"lambda")
        for d in input["holstein"]["lambda"]
            stddev = 0.0
            if haskey(d,"stddev")
                stddev = d["stddev"]
            end
            for orbit in d["orbit"]
                assign_λ!(holstein,d["val"],stddev,orbit)
            end
        end
    end

    # adding electron-phonon coupling
    if haskey(input["holstein"],"lambda2")
        for d in input["holstein"]["lambda2"]
            stddev = 0.0
            if haskey(d,"stddev")
                stddev = d["stddev"]
            end
            for orbit in d["orbit"]
                assign_λ₂!(holstein,d["val"],stddev,orbit)
            end
        end
    end

    # initialize model
    initialize_model!(holstein)

    return holstein
end

"""
Initialize SSH model from config file.
"""
function initialize_ssh_model(input::Dict,rng::AbstractRNG)

    # define lattice geometry
    unit_cell = UnitCell(input["lattice"]["ndim"],
                         input["lattice"]["norbits"],
                         hcat(input["lattice"]["lattice_vectors"]...),
                         hcat(input["lattice"]["basis_vectors"]...))

    # define lattice
    lattice = Lattice(unit_cell, input["lattice"]["L"])

    # restart for GMRES solver
    if haskey(input["solver"],"restart")
        restart = input["solver"]["restart"]
    else
        restart = 20
    end

    # initialize holstein model
    ssh = SSHModel(lattice,
                   input["ssh"]["beta"],
                   input["ssh"]["dtau"],
                   is_complex      = false,
                   iterativesolver = input["solver"]["type"],
                   rng             = rng,
                   tol             = input["solver"]["tol"],
                   maxiter         = input["solver"]["maxiter"],
                   restart         = restart)

    # adding chemical potential
    for d in input["ssh"]["mu"]
        stddev = 0.0
        if "stddev" in keys(d)
            stddev = d["stddev"]
        end
        for orbit in d["orbit"]
            assign_μ!(ssh,d["val"],stddev,orbit)
        end
    end

    # add hoppings
    if haskey(input["ssh"],"hopping")
        for d in input["ssh"]["hopping"]
            if haskey(d,"t_avg")
                t = d["t_avg"]
            else
                t = 0.0
            end
            if haskey(d,"t_std")
                σt = d["t_std"]
            else
                σt = 0.0
            end
            if haskey(d,"alpha_avg")
                α = d["alpha_avg"]
            else
                α = 0.0
            end
            if haskey(d,"alpha_std")
                σα = d["alpha_std"]
            else
                σα = 0.0
            end
            if haskey(d,"alpha2_avg")
                α₂ = d["alpha2_avg"]
            else
                α₂ = 0.0
            end
            if haskey(d,"alpha2_std")
                σα₂ = d["alpha2_std"]
            else
                σα₂ = 0.0
            end
            if haskey(d,"omega_avg")
                ω = d["omega_avg"]
            else
                ω = 0.0
            end
            if haskey(d,"omega_std")
                σω = d["omega_std"]
            else
                σω = 0.0
            end
            if haskey(d,"omega4_avg")
                ω₄ = d["omega4_avg"]
            else
                ω₄ = 0.0
            end
            if haskey(d,"omega4_std")
                σω₄ = d["omega4_std"]
            else
                σω₄ = 0.0
            end
            if haskey(d,"name")
                name = d["name"]
            else
                name = ""
            end
            o₁ = d["orbits"][1]
            o₂ = d["orbits"][2]
            dL = zeros(Int,3)
            dL[1:length(d["dL"])] .= d["dL"]
            assign_hopping!(ssh,t,σt,ω,σω,ω₄,σω₄,α,σα,α₂,σα₂,o₁,o₂,dL,name)
        end
    end

    # initialize model
    initialize_model!(ssh)

    return ssh
end

"""
Initialize Phonon Fields.
"""
function initialize_phonon_fields!(input::Dict,model::AbstractModel)

    # determine type of el-ph hamiltonian
    if haskey(input,"holstein")
        model_type = "holstein"
    else
        model_type= "ssh"
    end

    # intialize phonon field
    if !haskey(input[model_type],"read_phonon_config")
        input[model_type]["read_phonon_config"] = false
    end
    if input[model_type]["read_phonon_config"] # read in phonon field
        phononfile = input[model_type]["phonon_config_file"]
        read_phonons!(model, phononfile)
        cp(phononfile, joinpath(input["simulation"]["datafolder"],basename(phononfile)) )
    else # initialize to random phonon field
        init_phonons_half_filled!(model)
    end

    return nothing
end

"""
Initialize Preconditioner.
"""
function initialize_preconditioner(input::Dict,model::AbstractModel)

    # reconstruct preconditioner
    if haskey(input["solver"],"preconditioner")

        if haskey(input["solver"]["preconditioner"],"n")
            n = input["solver"]["preconditioner"]["n"]
        else
            n = 20
        end

        if haskey(input["solver"]["preconditioner"],"buf")
            buf = input["solver"]["preconditioner"]["buf"]
        else
            buf = 0.05
        end

        if haskey(input["solver"]["preconditioner"],"c1")
            c1 = input["solver"]["preconditioner"]["c1"]
        else
            c1 = 1.0
        end

        if haskey(input["solver"]["preconditioner"],"c2")
            c2 = input["solver"]["preconditioner"]["c2"]
        else
            c2 = 1.0
        end

        if lowercase(input["solver"]["type"])=="cg"
            preconditioner = SymmetricKPMPreconditioner(model,n,buf,c1,c2)
        else
            preconditioner = LeftRightKPMPreconditioner(model,n,buf,c1,c2)
        end
    else

        preconditioner = I
    end

    return preconditioner
end

"""
Initialize FourierAccelerator.
"""
function initialize_fourieraccelerator(input::Dict, model::AbstractModel)

    # defining FourierAccelerator type
    fa = FourierAccelerator(model)

    # set the mass used to construct fourier acceleration matrix
    for d in input["fourier_acceleration"]
        mass = d["mass"]
        if haskey(d,"c")
            c = d["c"]
        else
            c = 0.0
        end
        update_Q!(fa, model, d["omega_min"], d["omega_max"], mass)
        update_M!(fa, model, d["omega_min"], d["omega_max"], mass, c)
    end

    return fa
end

"""
Initialize SimulationParams.
"""
function initialize_simulation_params(input::Dict)

    if haskey(input,"hmc")
        meas_freq = input["hmc"]["meas_freq"]
        nsteps    = input["hmc"]["simulation_updates"]
        burnin    = input["hmc"]["burnin_updates"]
    else
        @assert input["langevin"]["burnin_timesteps"]%input["langevin"]["meas_freq"]==0
        meas_freq = input["langevin"]["meas_freq"]
        nsteps    = input["langevin"]["simulation_timesteps"]
        burnin    = input["langevin"]["burnin_timesteps"]
    end

    # set default checkpointing frequency in minutes
    if !haskey(input["simulation"],"checkpoint_freq")
        input["simulation"]["checkpoint_freq"]=10
    end

    # construct simulation parameters object.
    sim_params = SimulationParameters(burnin,
                                      nsteps,
                                      meas_freq,
                                      input["simulation"]["num_bins"],
                                      input["simulation"]["checkpoint_freq"],
                                      input["simulation"]["filepath"],
                                      input["simulation"]["foldername"],
                                      input["simulation"]["datafolder"])

    # make direcotory data will be written to
    if !isdir(sim_params.datafolder)
        mkdir(sim_params.datafolder)
    end

    # create log for simulation
    logfilename = joinpath(sim_params.datafolder, "$(sim_params.foldername).log")
    logio       = open(logfilename,"w+")
    logger      = SimpleLogger(logio)
    global_logger(logger)

    # log version info
    Pkg.status("ElPhDynamics",io=logger.stream)
    @info("Julia Version: $VERSION")
    flush(logger.stream)

    return sim_params
end

"""
Initialize Random Number Generator with Seed
"""
function initialize_rng(input::Dict)

    # initialize random number generator with seed
    if !haskey(input["simulation"],"random_seed")
        input["simulation"]["random_seed"] = abs(rand(Int))
    end
    seed = input["simulation"]["random_seed"]
    rng  = MersenneTwister(seed)

    # write random seed to log file
    @info("Random Seed: $seed")
    logger = global_logger()
    flush(logger.stream)

    return rng
end

"""
Initialize MuTuner.
"""
function initialize_mutuner(input::Dict,model::AbstractModel)

    if haskey(input,"tune_density")
        logfile        = joinpath(input["simulation"]["datafolder"],"mu_tuner_log.out")
        targed_density = input["tune_density"]["density"]
        memory         = input["tune_density"]["memory"]
        κ_min          = input["tune_density"]["kappa_min"]
        μ_tuner = MuTuner(true, mean(model.μ), targed_density*model.Nsites, model.Nsites, model.β, model.Δτ, memory, κ_min*model.Nsites, logfile)
    else
        μ_tuner = MuTuner(false, mean(model.μ), 1.0*model.Nsites, model.Nsites, model.β, model.Δτ, 0.75, 0.1, "")
    end

    return μ_tuner
end

"""
Initialize Dynamics.
"""
function initialize_dynamics(input::Dict,model::AbstractModel,burnin_start::Int=1,sim_start::Int=1)

    # check to bugs
    if haskey(input,"hmc") && haskey(input,"langevin")
        error("Config file cannot include both hmc and langevin tables.")
    end

    # number of degrees of freedom (phonon fields) to simulate
    NL = length(model)

    if haskey(input,"hmc")

        Δt              = input["hmc"]["dt"]
        tr              = input["hmc"]["trajectory_time"]
        α               = input["hmc"]["momentum_conservation_fraction"]
        Nb              = input["hmc"]["num_multitimesteps"]

        # log file instructions
        if haskey(input["hmc"],"log")
            log = input["hmc"]["log"]
        else
            log = false
        end
        if log && haskey(input["hmc"],"verbose")
            verbose = input["hmc"]["verbose"]
        else
            verbose = false
        end
        hmc_simulation_logfile = joinpath(input["simulation"]["datafolder"],"hmc_sim_log.out")
        hmc_burnin_logfile     = joinpath(input["simulation"]["datafolder"],"hmc_burnin_log.out")

        @assert 0.0 <= α < 1.0
        simulation_dynamics = HybridMonteCarlo(model, Δt, tr, α, Nb, log=log, verbose=verbose,
                                               logfilename=hmc_simulation_logfile,updates=sim_start)

        # defining burnin dynamics
        if haskey(input["hmc"], "burnin")
            if haskey(input["hmc"]["burnin"],"dt")
                Δt = input["hmc"]["burnin"]["dt"]
            end
            if haskey(input["hmc"]["burnin"],"trajectory_time")
                tr = input["hmc"]["burnin"]["trajectory_time"]
            end
            if haskey(input["hmc"]["burnin"],"momentum_conservation_fraction")
                α = input["hmc"]["burnin"]["momentum_conservation_fraction"]
            end
            if haskey(input["hmc"]["burnin"],"num_multitimesteps")
                Nb = input["hmc"]["burnin"]["num_multitimesteps"]
            end
            @assert 0.0 <= α < 1.0
        end
        burnin_dynamics = HybridMonteCarlo(simulation_dynamics, Δt, tr, α, Nb, log=log, verbose=verbose,
                                           logfilename=hmc_burnin_logfile, updates=burnin_start)

    elseif input["langevin"]["update_method"]==1

        Δt = input["langevin"]["dt"]
        simulation_dynamics = EulerDynamics(model,Δt)
        burnin_dynamics = simulation_dynamics

    elseif input["langevin"]["update_method"]==2

        Δt = input["langevin"]["dt"]
        simulation_dynamics = RungeKuttaDynamics(model,Δt)
        burnin_dynamics = simulation_dynamics

    elseif input["langevin"]["update_method"]==3

        Δt = input["langevin"]["dt"]
        simulation_dynamics = HeunsDynamics(model,Δt)
        burnin_dynamics = simulation_dynamics

    end

    return burnin_dynamics, simulation_dynamics
end

"""
Initialize Reflection Update.
"""
function initialize_reflect_update(input::Dict,model::AbstractModel,burnin_dynaimcs,simulation_dynamics)

    # define special update for simulation updates
    if haskey(input,"langevin")
        sim_reflect_update = NullUpdate()
    else
        if haskey(input,"holstein") && haskey(input["hmc"],"reflection_update")
            freq   = input["hmc"]["reflection_update"]["freq"]
            nsites = input["hmc"]["reflection_update"]["nsites"]
            sim_reflect_update = ReflectionUpdate(model,freq,nsites)
        else
            sim_reflect_update = NullUpdate()
        end
    end

    # define special update for burnin updates
    burnin_reflect_update = sim_reflect_update
    if haskey(input,"hmc") && haskey(input,"holstein")
        if haskey(input["hmc"],"burnin")
            if haskey(input["hmc"]["burnin"],"reflection_update")
                freq   = input["hmc"]["reflection_update"]["freq"]
                nsites = input["hmc"]["reflection_update"]["nsites"]
                burnin_reflect_update = ReflectionUpdate(model,freq,nsites)
            end
        end
    end

    return burnin_reflect_update, sim_reflect_update
end

"""
Initialize Swap Update.
"""
function initialize_swap_update(input::Dict,model::AbstractModel,burnin_dynaimcs,simulation_dynamics)

    # define special update for simulation updates
    if haskey(input,"langevin")
        sim_swap_update = NullUpdate()
    else
        if haskey(input,"holstein") && haskey(input["hmc"],"swap_update")
            freq   = input["hmc"]["swap_update"]["freq"]
            nbonds = input["hmc"]["swap_update"]["nbonds"]
            sim_swap_update = SwapUpdate(model,freq,nbonds)
        else
            sim_swap_update = NullUpdate()
        end
    end

    # define special update for burnin updates
    burnin_swap_update = sim_swap_update
    if haskey(input,"hmc") && haskey(input,"holstein")
        if haskey(input["hmc"],"burnin")
            if haskey(input["hmc"]["burnin"],"swap_update")
                freq   = input["hmc"]["swap_update"]["freq"]
                nbonds = input["hmc"]["swap_update"]["nbonds"]
                burnin_swap_update = SwapUpdate(model,freq,nbonds)
            end
        end
    end

    return burnin_swap_update, sim_swap_update
end

"""
Initialize Simulation Stats Dictionary.
"""
function initialize_sim_stats()::Dict

    sim_stats = Dict("simulation_time" => 0.0, "measurement_time" => 0.0, "write_time" => 0.0,
                     "iters" => 0.0, "acceptance_rate" => 0.0, "reflect_acceptance_rate" => 0.0,
                     "swap_acceptance_rate" => 0.0)

    return sim_stats
end

end


struct arf_struct
    exponent::UInt      # fmpz
    size::UInt          # mp_size_t
    mantissa1::UInt     # mantissa_struct of length 128
    mantissa2::UInt
end

struct arb_struct
                        # ┌ arf_struct (midpoint)
    exponent::UInt      # │ fmpz
    size::UInt          # │ mp_size_t
    mantissa1::UInt     # │ mantissa_struct of length 128
    mantissa2::UInt     # │
                        # └
                        # ┌ mag_struct (radius)
    exponent_mag::UInt  # │ fmpz
    mantissa_mag::UInt  # │ mp_limb_t
                        # └
end

struct acb_struct
                          # ┌ arb_struct (real)
    exponent_r::UInt      # │ fmpz
    size_r::UInt          # │ mp_size_t
    mantissa1_r::UInt     # │ mantissa_struct of length 128
    mantissa2_r::UInt     # │
    exp_mag_r::Int        # │ fmpz
    mantissa_mag_r::UInt  # │ mp_limb_t
                          # └
                          # ┌ arb_struct (imag)
    exponent_i::Int       # │ fmpz
    size_i::UInt          # │ mp_size_t
    mantissa1_i::UInt     # │ mantissa_struct of length 128
    mantissa2_i::UInt     # │
    exp_mag_i::Int        # │ fmpz
    mantissa_mag_i::UInt  # │ mp_limb_t
                          # └
end

struct mag_struct
    exponent::UInt       # fmpz
    mantissa::UInt       # mp_limb_t
end

for prefix in (:arf, :arb, :acb, :mag)
    arbstruct = Symbol(prefix, :_struct)
    arb_init = Symbol(prefix, :_init)
    arb_clear = Symbol(prefix, :_clear)
    @eval begin
        init!(t::$arbstruct) =
            ccall(@libarb($arb_init), Cvoid, (Ref{$arbstruct},), t)
        clear!(t::$arbstruct) =
            ccall(@libarb($arb_clear), Cvoid, (Ref{$arbstruct},), t)
    end
end


module TransformManager

export Transform

using ..Management
using CoreExtended
using MatrixMath

""" TODO """
type Transform
	id::Symbol
	enabled::Bool

  position::Vec3f
  rotation::Vec3f
	scaling::Vec3f

	Transform(id=:_) = new(id,true,zeros(Vec3f),zeros(Vec3f),ones(Vec3f))
end

# preset for manager: includes code here
#presetManager(Transform)
m=Management.presetManager(Transform)

end #TransformManager


module SpikeNetOpt

using PyCall
using OrderedCollections
using LinearAlgebra
using SpikeSynchrony
using SpikingNeuralNetworks
SNN = SpikingNeuralNetworks
SNN.@load_units
using Evolutionary
import DataStructures
using JLD
using UnicodePlots
using SpikeSynchrony
using Statistics
using JLD
using Distributed
using Plots
using UnicodePlots
using Distributions
using LightGraphs
using ClearStacktrace
using SharedArrays
using WaspNet
using SpikingNN
using Reexport

#using Plots

#using Metaheuristics

# Export Function names and struct names found in file utils
export get_vm
function get_vm end
function checkmodel end
include("utils.jl")
include("current_search.jl")
#include("sciunit.jl")

end


using HDF5
using Base
using ProgressMeter

function Loadh5Param(dir)
    h5file = dir * "ParamLLEJulia.h5"
    res = Dict()
    sim = Dict()
    sim_name = ["res", "sim"]
    par = [["Qc", "R", "ng", "Qi", "gamma","dispfile"], ["dphi","Pin", "Tscan", "domega_init", "domega_end", "f_pmp", "mu_sim", "debug", "ind_aux"]]
    cnt = 1
    for sim_par = [res, sim]
        for it = par[cnt]
            sim_par[it] = h5read(h5file, sim_name[cnt] *"/" * it)
        end
        cnt += 1
    end

    return res, sim
end

function SaveResults(dir, S)
    h5file = dir * "ResultsJulia.h5"
    # print(h5file)
    # print("\n")
    h5open(h5file, "w") do file
        g = g_create(file, "Results") # create a group
        for ii in S
            g[ii[1]*"Real"] = real(ii[2])              # create a scalar dataset inside the group
            g[ii[1]*"Imag"] = imag(ii[2])
        end
        attrs(g)["Description"] = "This group contains only a single dataset" # an attribute
    end
        # attrs(g)["Description"] = "This group contains only a single dataset" # an attribute
end

# ----------------------------------------------------
# -- STARTING MAIN SCRIPT --
# ----------------------------------------------------
# FFTW.set_num_threads(Sys.CPU_CORES)

tmp_dir = ARGS[1]
tol= float(ARGS[2])
maxiter = Int(float(ARGS[3]))
stepFactor = float(ARGS[4])
res, sim = Loadh5Param(tmp_dir)
logfile =  open(tmp_dir * "Log.log" ,"w")

# -- Collect Data ---
# ----------------------------------------------------
c0 = 299792458
ħ = 6.634e-34/2/pi

# -- res parameters --
ng = res["ng"][1]
R = res["R"][1]
γ = res["gamma"][1]
L = 2*pi*R

Q0 = res["Qi"][1]
Qc = res["Qc"][1]

# -- sim parameters --
debug = Bool(sim["debug"][1])
δω_init = sim["domega_init"][1]
δω_end = sim["domega_end"][1]
t_end = sim["Tscan"][1]
fpmp = sim["f_pmp"][1]
Pin = sim["Pin"][1]
if length(sim["f_pmp"])>1
    faux = sim["f_pmp"][2:length(sim["f_pmp"])]
    Paux = sim["Pin"][2:length(sim["f_pmp"])]
else
    faux = []
    Paux = []
end
ω0 = 2*pi*fpmp
dφ = sim["dphi"]
μ_sim = sim["mu_sim"]
μ = collect(μ_sim[1]:μ_sim[2])
pmp_sim = find(μ .== 0)[1]
ind_aux = sim["ind_aux"]
write(logfile,string(ind_aux))
print(string(ind_aux))
# -- Setup the different Parameters --
# ----------------------------------------------------
tR = L*ng/c0
f0 = c0/ng/L
T = 1*tR
θ = ω0/Qc*tR
α=1/2* (ω0/Q0 + ω0/Qc) * tR

# -- losses --
dω = collect(μ_sim[1]:μ_sim[end])*2*pi/T
Qc_disp=Qc*ones(size(dω))
Qi_disp=Q0*ones(size(dω))

# -- Dispersion --
dβ=dφ/L
dβ=dβ-dβ[pmp_sim]
dφ_Kerr=θ*Pin*γ*L/α^2

# -- Input Energy --
Ein= zeros(size(μ))
Ein[pmp_sim]=sqrt.(Pin)*length(μ)

for ii=1:length(faux)
    Ein[pmp_sim - ind_aux[ii]] = sqrt.(Paux[ii])*length(μ)
end
Ein_couple=sqrt(θ).*Ein

# -- Noise Background --
Ephoton=ħ*(ω0+dω)
phase=2*pi*(rand(1,length(μ)))
array=rand(1,length(μ))
Enoise=array'.*sqrt.(Ephoton/2/tR).*exp.(1im*phase') .*length(μ)

dt=0.1/(sqrt(Pin))
δω_init = δω_init * tR
δω_end = δω_end * tR

t_end = t_end*tR
t_ramp=t_end

# -- Seting up Simulation --
# ----------------------------------------------------

# -- Allocate FFT to go faster --
ifft_plan = plan_ifft(zeros(size(Enoise)))
fft_plan = plan_fft(zeros(size(Enoise)))

# -- Sim length --
tol=1e-3
# tol=1e-20
maxiter=6
Nt= round(t_ramp/tR/dt)
t1=0

# -- Detuning ramp --
xx = collect(1:Nt)
Δω_pmp = δω_init+ xx/Nt * (δω_end- δω_init)

# -- Initiial State --
u0=ifft_plan*Enoise

# -- Output Dict --
S = Dict()
num_probe=1000
S["u_probe"] = 1im*zeros(num_probe, length(u0))
S["Em_probe"] = 1im*zeros(num_probe, length(u0))
S["comb_power"] = zeros(num_probe,)
S["detuning"] = zeros(num_probe,)

# -- Progress Bar --
probe=0
# pb = Progress(Int(Nt),0.5, "Calculating temporal loop:", Int(25))

space = "."^100
logfile =  open(tmp_dir * "log.log" ,"a")
write(logfile,string(Int(round(0))) * "\n")
close(logfile)
probe_pbar = 1/100
cnt_pbar = 0
# -- Main Solver --
# ----------------------------------------------------
for it = 1:1:Nt
    # print("coucouc\n")
    if it/Nt >=probe_pbar
        cnt_pbar = cnt_pbar + 1
        # print("\b"^103)
        # print("coucou")
        # print("="^cnt_pbar)
        # print("."^(space-cnt_pbar))
        # print(" ")
        logfile =  open(tmp_dir * "log.log" ,"a")
        write(logfile,string(Int(round(probe_pbar*100))) * "\n")
        close(logfile)
        probe_pbar = probe_pbar + 0.01
    end
    u0=ifft_plan*(fft_plan*(u0) + Ein_couple*dt)
    u1=u0
    cbeta = -α  + 1im*Δω_pmp[Int(it)] +  1im*L*dβ
    halfprop = exp.(cbeta*dt/2)
    cnt = 0
    uhalf = ifft_plan*(halfprop.*(fft_plan*(u0)))
    half1 = ifft_plan*(γ*(fft_plan*( abs.(u0).^2.*u0) ) )./u0
    for ii = 1:maxiter
        half2 = ifft_plan*(γ*(fft_plan*( abs.(u1).^2.*u1)))./u1
        uv = uhalf .* exp.(1im*L.*(half1 + half2)*dt/2)
        uv2 = ifft_plan*(halfprop.*(fft_plan*(uv)))
        if (norm(uv2-u1,2)/norm(u1,2) < tol)
            u1 = uv2
            break
        else
            u1 = uv2
        end
        cnt += 1
    end

    if (cnt == maxiter)
        logfile =  open(tmp_dir * "log.log" ,"a")
        write(logfile,"Failed to converge to N=" * string(it) * " over Nt=" * string(Nt))
        close(logfile)
    end
    u0=u1

   if (it*num_probe/Nt > probe)
        probe += 1
        S["u_probe"][probe,:]=u0[:,1]
        Em_probe = (fft_plan*(u0))/length(μ)
        Em_probe = Em_probe[:,1]
        S["Em_probe"][probe,:]= Em_probe
        deleteat!(Em_probe,pmp_sim)
        S["comb_power"][probe]=(sum(abs.(Em_probe).^2))/Pin
        S["detuning"][probe] = Δω_pmp[Int(it)]/tR
   end

end
S["Ewg"] = 1im*zeros(size(S["Em_probe"]))
for ii=1:size(S["Em_probe"],1)
    S["Ewg"][ii,:] = Ein/length(μ)-S["Em_probe"][ii,:].*sqrt(θ)
end
S["ω"] = (ω0 + dω)

# print("\n")
# print("Congrats, everything has been solved.\n")
# print("Now we are going to save the results in a .h5 file\n")
# print("Everything will be in: ")
logfile =  open(tmp_dir * "log.log" ,"a")
write(logfile,string(Int(round(100))) * "\n")
close(logfile)

SaveResults(tmp_dir, S)
# write(logfile,"Great we have saved everything... Moving back to Python\n")


"""
    cnse_boundary_state!(::NumericalFluxFirstOrder, ::Impenetrable{FreeSlip}, ::CNSE2D)

apply free slip boundary condition for momentum
sets reflective ghost point
"""
@inline function cnse_boundary_state!(
    ::NumericalFluxFirstOrder,
    ::Impenetrable{FreeSlip},
    ::CNSE2D,
    ::TurbulenceClosure,
    state⁺,
    aux⁺,
    n⁻,
    state⁻,
    aux⁻,
    t,
    args...,
)
    state⁺.ρ = state⁻.ρ

    ρu⁻ = @SVector [state⁻.ρu[1], state⁻.ρu[2], -0]
    ρu⁺ = ρu⁻ - 2 * n⁻ ⋅ ρu⁻ .* SVector(n⁻)

    state⁺.ρu = @SVector [ρu⁺[1], ρu⁺[2]]

    return nothing
end

"""
    cnse_boundary_state!(::Union{NumericalFluxGradient, NumericalFluxSecondOrder}, ::Impenetrable{FreeSlip}, ::CNSE2D)

no second order flux computed for linear drag
"""
cnse_boundary_state!(
    ::Union{NumericalFluxGradient, NumericalFluxSecondOrder},
    ::MomentumBC,
    ::CNSE2D,
    ::LinearDrag,
    _...,
) = nothing

"""
    cnse_boundary_state!(::NumericalFluxGradient, ::Impenetrable{FreeSlip}, ::CNSE2D)

apply free slip boundary condition for momentum
sets non-reflective ghost point
"""
function cnse_boundary_state!(
    ::NumericalFluxGradient,
    ::Impenetrable{FreeSlip},
    ::CNSE2D,
    ::ConstantViscosity,
    state⁺,
    aux⁺,
    n⁻,
    state⁻,
    aux⁻,
    t,
    args...,
)
    state⁺.ρ = state⁻.ρ

    ρu⁻ = @SVector [state⁻.ρu[1], state⁻.ρu[2], -0]
    ρu⁺ = ρu⁻ - n⁻ ⋅ ρu⁻ .* SVector(n⁻)

    state⁺.ρu = @SVector [ρu⁺[1], ρu⁺[2]]

    return nothing
end

"""
    shallow_normal_boundary_flux_second_order!(::NumericalFluxSecondOrder, ::Impenetrable{FreeSlip}, ::CNSE2D)

apply free slip boundary condition for momentum
apply zero numerical flux in the normal direction
"""
function cnse_boundary_state!(
    ::NumericalFluxSecondOrder,
    ::Impenetrable{FreeSlip},
    ::CNSE2D,
    ::ConstantViscosity,
    state⁺,
    gradflux⁺,
    aux⁺,
    n⁻,
    state⁻,
    gradflux⁻,
    aux⁻,
    t,
    args...,
)
    state⁺.ρu = state⁻.ρu
    gradflux⁺.ν∇u = n⁻ * (@SVector [-0, -0])'

    return nothing
end

"""
    cnse_boundary_state!(::NumericalFluxFirstOrder, ::Impenetrable{NoSlip}, ::CNSE2D)

apply no slip boundary condition for momentum
sets reflective ghost point
"""
@inline function cnse_boundary_state!(
    ::NumericalFluxFirstOrder,
    ::Impenetrable{NoSlip},
    ::CNSE2D,
    ::TurbulenceClosure,
    state⁺,
    aux⁺,
    n⁻,
    state⁻,
    aux⁻,
    t,
    args...,
)
    state⁺.ρ = state⁻.ρ
    state⁺.ρu = -state⁻.ρu

    return nothing
end

"""
    cnse_boundary_state!(::NumericalFluxGradient, ::Impenetrable{NoSlip}, ::CNSE2D)

apply no slip boundary condition for momentum
set numerical flux to zero for U
"""
@inline function cnse_boundary_state!(
    ::NumericalFluxGradient,
    ::Impenetrable{NoSlip},
    ::CNSE2D,
    ::ConstantViscosity,
    state⁺,
    aux⁺,
    n⁻,
    state⁻,
    aux⁻,
    t,
    args...,
)
    FT = eltype(state⁺)
    state⁺.ρu = @SVector zeros(FT, 2)

    return nothing
end

"""
    cnse_boundary_state!(::NumericalFluxSecondOrder, ::Impenetrable{NoSlip}, ::CNSE2D)

apply no slip boundary condition for momentum
sets ghost point to have no numerical flux on the boundary for U
"""
@inline function cnse_boundary_state!(
    ::NumericalFluxSecondOrder,
    ::Impenetrable{NoSlip},
    ::CNSE2D,
    ::ConstantViscosity,
    state⁺,
    gradflux⁺,
    aux⁺,
    n⁻,
    state⁻,
    gradflux⁻,
    aux⁻,
    t,
    args...,
)
    state⁺.ρu = -state⁻.ρu
    gradflux⁺.ν∇u = gradflux⁻.ν∇u

    return nothing
end

"""
    cnse_boundary_state!(::Union{NumericalFluxFirstOrder, NumericalFluxGradient}, ::Penetrable{FreeSlip}, ::CNSE2D)

no mass boundary condition for penetrable
"""
cnse_boundary_state!(
    ::Union{NumericalFluxFirstOrder, NumericalFluxGradient},
    ::Penetrable{FreeSlip},
    ::CNSE2D,
    ::ConstantViscosity,
    _...,
) = nothing

"""
    cnse_boundary_state!(::NumericalFluxSecondOrder, ::Penetrable{FreeSlip}, ::CNSE2D)

apply free slip boundary condition for momentum
apply zero numerical flux in the normal direction
"""
function cnse_boundary_state!(
    ::NumericalFluxSecondOrder,
    ::Penetrable{FreeSlip},
    ::CNSE2D,
    ::ConstantViscosity,
    state⁺,
    gradflux⁺,
    aux⁺,
    n⁻,
    state⁻,
    gradflux⁻,
    aux⁻,
    t,
    args...,
)
    state⁺.ρu = state⁻.ρu
    gradflux⁺.ν∇u = n⁻ * (@SVector [-0, -0])'

    return nothing
end

"""
    cnse_boundary_state!(::Union{NumericalFluxFirstOrder, NumericalFluxGradient}, ::Impenetrable{MomentumFlux}, ::HBModel)

apply kinematic stress boundary condition for momentum
applies free slip conditions for first-order and gradient fluxes
"""
function cnse_boundary_state!(
    nf::Union{NumericalFluxFirstOrder, NumericalFluxGradient},
    ::Impenetrable{<:MomentumFlux},
    model::CNSE2D,
    turb::TurbulenceClosure,
    args...,
)
    return cnse_boundary_state!(
        nf,
        Impenetrable(FreeSlip()),
        model,
        turb,
        args...,
    )
end

"""
    cnse_boundary_state!(::NumericalFluxSecondOrder, ::Impenetrable{MomentumFlux}, ::HBModel)

apply kinematic stress boundary condition for momentum
sets ghost point to have specified flux on the boundary for ν∇u
"""
@inline function cnse_boundary_state!(
    ::NumericalFluxSecondOrder,
    ::Impenetrable{<:MomentumFlux},
    model::CNSE2D,
    state⁺,
    gradflux⁺,
    aux⁺,
    n⁻,
    state⁻,
    gradflux⁻,
    aux⁻,
    t,
)
    state⁺.ρu = state⁻.ρu
    gradflux⁺.ν∇u = n⁻ * bc.drag(state⁻, aux⁻, t)'

    return nothing
end

"""
    cnse_boundary_state!(::Union{NumericalFluxFirstOrder, NumericalFluxGradient}, ::Penetrable{MomentumFlux}, ::HBModel)

apply kinematic stress boundary condition for momentum
applies free slip conditions for first-order and gradient fluxes
"""
function cnse_boundary_state!(
    nf::Union{NumericalFluxFirstOrder, NumericalFluxGradient},
    ::Penetrable{<:MomentumFlux},
    model::CNSE2D,
    turb::TurbulenceClosure,
    args...,
)
    return cnse_boundary_state!(
        nf,
        Penetrable(FreeSlip()),
        model,
        turb,
        args...,
    )
end

"""
    cnse_boundary_state!(::NumericalFluxSecondOrder, ::Penetrable{MomentumFlux}, ::HBModel)

apply kinematic stress boundary condition for momentum
sets ghost point to have specified flux on the boundary for ν∇u
"""
@inline function cnse_boundary_state!(
    ::NumericalFluxSecondOrder,
    bc::Penetrable{<:MomentumFlux},
    shallow::CNSE2D,
    ::TurbulenceClosure,
    state⁺,
    gradflux⁺,
    aux⁺,
    n⁻,
    state⁻,
    gradflux⁻,
    aux⁻,
    t,
)
    state⁺.ρu = state⁻.ρu
    gradflux⁺.ν∇u = n⁻ * bc.drag(state⁻, aux⁻, t)'

    return nothing
end


function load_board(str, go_env::GoEnv)
  reverse_map = Dict{Char, Int}([
      'X' => 1, # BLACK
      'O' => -1,  # WHITE
      '.' => 0, # EMPTY
      '#' => 2, # FILL
      '*' => 3, # Ko
      '?' => 4  # UNKNOWN
  ])
  str = replace(str, r"[^XO\.#]+" => s"")
  @assert length(str) == go_env.N ^ 2 # "Board to load didn't have right dimensions"
  board = zeros(Int8, go_env.N, go_env.N)
  for (i, char) in enumerate(str)
      board[i] = reverse_map[char]
  end
  board = permutedims(board, [2, 1])
  return board
end

function assertEqualArray(array1, array2)
  if !all(array1 .== array2)
    throw(AssertionError("Arrays differed in one or more locations:\n$array1\n%$array2\n"))
  end
  return true
end

function assertEqualLibTracker(lib_tracker1, lib_tracker2)
  # A lib tracker may have differently numbered groups yet still
  # represent the same set of groups.
  # "Sort" the group_ids to ensure they are the same.
  function find_group_mapping(lib_tracker)
    current_gid = 0
    mapping = Dict{Int16, Int}()
    for group_id in lib_tracker.group_index
      if group_id == MISSING_GROUP_ID
        continue
      end
      if group_id ∉ keys(mapping)
        mapping[group_id] = current_gid
        current_gid += 1
      end
    end
    return mapping
  end

  lt1_mapping = find_group_mapping(lib_tracker1)
  lt2_mapping = find_group_mapping(lib_tracker2)

  remapped_group_index1 = [get(lt1_mapping, gid, MISSING_GROUP_ID)
                            for gid in lib_tracker1.group_index]
  remapped_group_index2 = [get(lt2_mapping, gid, MISSING_GROUP_ID)
                            for gid in lib_tracker2.group_index]
  @assert remapped_group_index1 == remapped_group_index2

  remapped_groups1 = Dict(get(lt1_mapping, gid, 0) => group for (gid, group) in lib_tracker1.groups)
  remapped_groups2 = Dict(get(lt2_mapping, gid, 0) => group for (gid, group) in lib_tracker2.groups)
  @assert remapped_groups1 == remapped_groups2

  assertEqualArray(lib_tracker1.liberty_cache, lib_tracker2.liberty_cache)
end

function assertEqualPositions(pos1, pos2)
  assertEqualArray(pos1.board, pos2.board)
  assertEqualLibTracker(pos1.lib_tracker, pos2.lib_tracker)
  @assert pos1.n == pos2.n
  @assert pos1.caps == pos2.caps
  @assert pos1.ko == pos2.ko
  r_len = min(length(pos1.recent), length(pos2.recent))
  if r_len > 0 # if a position has no history, then don't bother testing
    @assert pos1.recent[end - r_len + 1:end] == pos2.recent[end - r_len + 1:end]
  end
  @assert pos1.to_play == pos2.to_play
  return true
end

function assertNoPendingVirtualLosses(root)
  # Raise an error if any node in this subtree has vlosses pending.
  queue = [root]
  while !isempty(queue)
    current = pop!(queue)
    @assert current.losses_applied == 0
    queue = vcat(queue, collect(values(current.children)))
  end
  return true
end


struct Mesh <: FEMContainer
    coords::Array{Float64}
    # blocks::Array{Block}
    blocks::Blocks
    node_sets::Array{NodeSet}
    # add side sets and other relevant stuff
    #
end


function bundleTMs!(df, nBundles)
    df.x += floor.((df.structMap .- 1) ./ nBundles) .* 2
end


### Generalized Metropolis-Hastings Monte Carlo runner
immutable GMHRunner <: AbstractMHRunner
    numburnin::Int
    numiterations::Int
    numproposals::Int
    numindicatorsamples::Int
    policy::MHRuntimePolicy #policies determining runtime behaviour of the MCMC

    function GMHRunner(nburnin::Int,niterations::Int,nproposals::Int,nindsamples::Int,policy::MHRuntimePolicy)
        @assert 0 <= nburnin && 0 < niterations && 0 < nproposals && 0 < nindsamples
        new(nburnin,niterations,nproposals,nindsamples,policy)
    end
end

###Factory functions
_runner(::Type{Val{:generalized}},policy::MHRuntimePolicy,niterations::Int,nproposals::Int;numburnin =0,numindicatorsamples =nproposals) = GMHRunner(numburnin,niterations,nproposals,numindicatorsamples,policy)

function run!(runner_::GMHRunner,model_::AbstractModel,sampler_::AbstractSampler,tuner_::AbstractTuner)
    indicatorstate_ = samplerstate(sampler_,1,runner_.policy.sampletype,runner_.policy.calculationtype)
    segments_ = remotesegments(runner_.policy,model_,sampler_,runner_.numproposals)
    show(segments_)
    indicator_,tunerstate_,chain_ = createcommon(runner_,tuner_,numparas(model_),numtotalproposals(segments_),runner_.numindicatorsamples)
    tic()
    indicatorstate_ = burnin!(runner_,model_,sampler_,indicatorstate_,segments_,tuner_,tunerstate_,indicator_,chain_)
    indicatorstate_ = main!(runner_,model_,sampler_,indicatorstate_,segments_,tuner_,tunerstate_,indicator_,chain_)
    chain_.runtime = toq()
    chain_
end

function burnin!(runner_::GMHRunner,model_::AbstractModel,sampler_::AbstractSampler,indicatorstate_::AbstractSamplerState,segments_::AbstractRemoteSegments,
                 tuner_::AbstractTuner,tunerstate_::AbstractTunerState,indicator_::AbstractIndicatorMatrix,chain_::AbstractChain)
    println("==============")
    println("Burnin started")
    println("==============")
    showsamplerstatevars(indicatorstate_,"indicator state")
    println("==================================")
    storeduring = _storeduring(:burnin,runner_.policy)
    initialize!(runner_,model_,indicatorstate_,chain_,storeduring)
    for i=1:runner_.numburnin
        iterate!(runner_,model_,indicatorstate_,segments_,indicator_,chain_,storeduring)
        accepted!(tunerstate_,indicator_)
        needstuning(tuner_,i)?tune!(runner_,sampler_,indicatorstate_,segments_,tuner_,tunerstate_):nothing
        indicatorstate_ = preparenext!(runner_,model_,indicatorstate_,segments_,indicator_) #prepare for the next iteration
    end
    println("================")
    println("Burnin completed")
    println("================")
    showsamplerstatevars(indicatorstate_,"indicator state")
    println("==================================")
    indicatorstate_
end

function main!(runner_::GMHRunner,model_::AbstractModel,sampler_::AbstractSampler,indicatorstate_::AbstractSamplerState,segments_::AbstractRemoteSegments,
               tuner_::AbstractTuner,tunerstate_::AbstractTunerState,indicator_::AbstractIndicatorMatrix,chain_::AbstractChain)
    println("================")
    println("Main run started")
    println("================")
    showsamplerstatevars(indicatorstate_,"indicator state")
    println("==================================")
    storeduring = _storeduring(:main,runner_.policy)
    for i=1:runner_.numiterations
        iterate!(runner_,model_,indicatorstate_,segments_,indicator_,chain_,storeduring)
        needstuning(tuner_,i)?println("Iteration $i/$(runner_.numiterations)"):nothing
        indicatorstate_ = preparenext!(runner_,model_,indicatorstate_,segments_,indicator_) #prepare for the next iteration
    end
    println("==================")
    println("Main run completed")
    println("==================")
    showsamplerstatevars(indicatorstate_,"indicator state")
    println("==================================")
    indicatorstate_
end

function iterate!(runner_::GMHRunner,model_::AbstractModel,indicatorstate_::AbstractSamplerState,segments_::AbstractRemoteSegments,
                  indicator_::AbstractIndicatorMatrix,chain_::AbstractChain,storeduring::Bool)
    auxiliary!(runner_,model_,indicatorstate_)
    indicatoracceptance = acceptance!(indicatorstate_)
    segmentacceptances = iterate!(segments_,indicatorstate_)
    transitionprobability!(indicator_,indicatoracceptance,segmentacceptances)
    sampleindicator!(indicator_)
    storeduring?store!(runner_,indicatorstate_,segments_,indicator_,chain_):nothing
end

function preparenext!(runner_::GMHRunner,model_::AbstractModel,indicatorstate_::AbstractSamplerState,
                  segments_::AbstractRemoteSegments,indicator_::AbstractIndicatorMatrix)
    indicatorend = indicatorsamples(indicator_)[end]
    if indicatorend != numproposals(indicator_) + 1
        indicatorstate_ = prepare!(segments_,indicatorstate_,indicatorend)
    else
        indicatorstate_ = prepareindicator!(indicatorstate_)
    end
    traitvalue(runner_.policy.model)==:stochastic?geometry!(model_,from(indicatorstate_)):nothing
    indicatorstate_
end

function auxiliary!(runner_::GMHRunner,model_::AbstractModel,indicatorstate_::AbstractSamplerState)
    auxcounter = 0
    while ~isfinite((propose!(indicatorstate_) ; geometry!(model_,proposals(indicatorstate_))).logprior[1])
        if (auxcounter+=1) > maxinitialcounter
            error("Problems generating auxiliary proposal, giving up. Please check the parameter priors.")
        end
    end
    indicatorstate_
end

function store!(runner_::GMHRunner,indicatorstate_::AbstractSamplerState,segments_::AbstractRemoteSegments,
                indicator_::AbstractIndicatorMatrix,chain_::AbstractChain)
    nproposals = numproposals(indicator_)
    nsamples = numsamples(indicator_)
    indicatorsamples_ = indicatorsamples(indicator_)
    retrievesamples!(segments_,indicatorsamples_)
    from_ = from(indicatorstate_)
    for i=2:nsamples+1
        @inbounds if indicatorsamples_[i] == nproposals + 1
            store!(chain_,from_,1)
        else
            @inbounds store!(segments_,chain_,indicatorsamples_[i])
        end
    end
    accepted!(chain_,indicator_)
end

function tune!(runner_::AbstractRunner,sampler_::AbstractSampler,indicatorstate_::AbstractSamplerState,
               segments_::AbstractRemoteSegments,tuner_::AbstractTuner,tunerstate_::AbstractTunerState)
    tvals = tune(tuner_,tunerstate_)
    showstep(tuner_,tunerstate_)
    tune!(indicatorstate_,tvals...)
    tune!(segments_,tvals...)
    nextindex!(tunerstate_)
end

function show(io::IO,r::GMHRunner)
    println(io,"Generalized Metropolis-Hastings runner with:")
    println(io," numburnin: ",r.numburnin)
    println(io," numiterations: ",r.numiterations)
    println(io," numproposals: ",r.numproposals)
    println(io," numindicatorsamples: ",r.numindicatorsamples)
    print(io," policy: ")
    show(io,r.policy)
    println(io)
    nothing
end


function getRobject(file::String, name::String)
    R"""
    library(nodiv)
    load($file)
    """
    getRobject(name)
end

function getRobject(name::String)
    R"""
    obj = get($name)
    if(!inherits(obj, "distrib_data"))
        stop(paste(name, "is not a distrib_data object!"))

    obj$shape <- NULL
    obj$sitestat = obj$coords@data
    obj$coords = sp::coordinates(obj$coords)
    """
    rcopy(R"obj")
end

function SpatialEcology.Assemblage(rdict::Dict{T, S}) where {T <: Symbol, S}

    cd_type = rdict[:type] == "grid" ? griddata : pointdata #this code only works as long as there are only those two types
    Assemblage(rdict[:comm], rdict[:coords] ,
        cdtype = cd_type, traits = rdict[:species_stats],
        sitestats = rdict[:sitestat])
end


##########################################################################
# Copyright 2017 Samuel Ridler.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##########################################################################

# integer program formulation by Oddo Zhang, for move-up
# From thesis: "Simulation optimisation and Markov models for dynamic ambulance redeployment"

# initialise data relevant to move up
function initZhangIp!(sim::Simulation, zhangIpData::ZhangIpData)
	zid = sim.moveUpData.zhangIpData = zhangIpData # shorthand

	# check number of stations
	numStations = sim.numStations
	@assert(length(zid.marginalBenefits) == numStations)
	@assert(length(zid.stationCapacities) == numStations)

	# check station capacities
	for i = 1:numStations
		@assert(zid.stationCapacities[i] <= sim.stations[i].capacity)
	end

	zid.stationSlots = []
	zid.benefitSlots = []
	for i = 1:numStations
		for j = 1:zid.stationCapacities[i]
			push!(zid.stationSlots, i)
			push!(zid.benefitSlots, zid.marginalBenefits[i][j])
		end
	end

	zid.marginalBenefitsDecreasing = all(i -> issorted(zid.marginalBenefits[i], lt=<=, rev=true), 1:numStations)
	if !zid.marginalBenefitsDecreasing
		p1 = vcat([findall(zid.stationSlots .== j)[1:end-1] for j = 1:numStations]...)
		p2 = vcat([findall(zid.stationSlots .== j)[2:end] for j = 1:numStations]...)
		zid.stationSlotsOrderPairs = hcat(p1, p2)
	end

	@assert(sim.travel.numSets == 1) # otherwise, need to be careful in calculating the regret travel time for move up of on-road ambulances
end
function initZhangIp!(sim::Simulation; paramsFilename::String = "")
	zid = readZhangIpParamsFile(paramsFilename)
	initZhangIp!(sim, zid)
end

# determine move ups to make at current time
# returns list of ambulances to be moved, and list of their destinations (stations)
function zhangIpMoveUp(sim::Simulation)::Tuple{Vector{Ambulance}, Vector{Station}}

	# shorthand names:
	zid = sim.moveUpData.zhangIpData
	@unpack stationSlots, benefitSlots = zid
	@unpack ambulances, stations, numStations = sim

	# get currently movable ambulances, and at-hospital ambulances
	movableAmbs = filter(a -> isAmbMovable(a), ambulances)
	atHospitalAmbs = filter(a -> a.status == ambAtHospital, ambulances)
	@assert(!any(a -> isAmbMovable(a) && a.status == ambAtHospital, ambulances)) # @assert(isempty(intersect(movableAmbs, atHospitalAmbs))) is much slower
	if isempty(movableAmbs) return moveUpNull() end

	# let moveUpAmbs be the ambulances that can be moved now, and those that can be moved later (currently at-hospital)
	moveUpAmbs = vcat(movableAmbs, atHospitalAmbs)
	numMoveUpAmbs = length(moveUpAmbs)

	# calculate travel time for each move-up ambulance to reach every station
	ambToStationTimes = Array{Float,2}(undef, numMoveUpAmbs, numStations)
	for i = 1:numMoveUpAmbs
		ambToStationTimes[i,:] = ambMoveUpTravelTimes!(sim, moveUpAmbs[i])
	end

	# calculate adjustedAmbToStationTimes from ambToStationTimes, according to Zhang's thesis
	# for at-station or newly-freed ambulances: adjusted time = original time
	# for on-road ambulances: adjusted time = original time * ( time spent on route + time to drive to station - time from route origin to station <= regret-travel-time threshold ? discount factor : 1 )
	# for at-hospital ambulances: adjusted time = original time + expected remaining handover duration
	adjustedAmbToStationTimes = copy(ambToStationTimes)
	travelMode = getTravelMode!(sim.travel, lowPriority, sim.time)
	for i = 1:numMoveUpAmbs
		ambulance = moveUpAmbs[i]
		if ambulance.status == ambIdleAtStation || ambulance.status == ambFreeAfterCall
			# no change, adjustedAmbToStationTimes[i,:] = ambToStationTimes[i,:]
		elseif isGoingToStation(ambulance.status)
			for j = 1:numStations
				# calculate "regret" travel time
				if ambulance.stationIndex == j
					regretTravelTime = 0.0
				else
					# get travel time from ambulance.route.startLoc to station j
					time1 = ambulance.route.startFNodeTime - ambulance.route.startTime
					pathTravelTime = shortestPathTravelTime(sim.net, travelMode.index, ambulance.route.startFNode, stations[j].nearestNodeIndex)
					time2 = offRoadTravelTime(travelMode, stations[j].nearestNodeDist)
					tob = time1 + pathTravelTime + time2 # on and off road travel times

					tox = sim.time - ambulance.route.startTime # time spent on current route
					txb = ambToStationTimes[i,j]
					regretTravelTime = tox + txb - tob
				end

				# apply discount if regret travel time is within threshold
				if regretTravelTime <= zid.regretTravelTimeThreshold
					adjustedAmbToStationTimes[i,j] *= zid.onRoadMoveUpDiscountFactor
				end
			end
		elseif ambulance.status == ambAtHospital
			adjustedAmbToStationTimes[i,:] .+= zid.expectedHospitalHandoverDuration
		else
			error()
		end
	end

	ambToStationCosts = adjustedAmbToStationTimes * zid.travelTimeCost

	# solve
	if zid.marginalBenefitsDecreasing
		# solve as assignment problem
		(movableAmbStations, atHospitalAmbStations) = solveZhangIpAssignmentProblem(stationSlots, benefitSlots, ambToStationCosts, length(movableAmbs), length(atHospitalAmbs))
	else
		# solve as an integer program
		(movableAmbStations, atHospitalAmbStations) = solveZhangIp(stationSlots, benefitSlots, ambToStationCosts, length(movableAmbs), length(atHospitalAmbs), zid.marginalBenefitsDecreasing;
			stationSlotsOrderPairs = zid.stationSlotsOrderPairs)
	end

	ambStations = stations[movableAmbStations]

	return movableAmbs, ambStations
end

# solve as an integer program
function solveZhangIp(stationSlots::Vector{Int}, benefitSlots::Vector{Float}, ambToStationCosts::Array{Float,2},
	numMovableAmbs::Int, numAtHospitalAmbs::Int,
	marginalBenefitsDecreasing::Bool; stationSlotsOrderPairs::Array{Int,2} = Array{Int,2}(undef,0,0))::Tuple{Vector{Int}, Vector{Int}}
	# ambToStationCosts[:,j] gives costs to move different ambulances to station j
	# ambToStationCosts[1:numMovableAmbs,:] are for ambs that can be moved now
	# ambToStationCosts[(1:numAtHospitalAmbs).+numMovableAmbs,:] are for ambs that are at hospital
	# stationSlotsOrderPairs is not needed if marginalBenefitsDecreasing = true

	(numMoveUpAmbs, numStations) = size(ambToStationCosts)
	if numMoveUpAmbs == 0 return (Int[], Int[]) end
	numStationSlots = length(stationSlots)
	@assert(numMoveUpAmbs == numMovableAmbs + numAtHospitalAmbs)
	@assert(numStationSlots >= numMoveUpAmbs)
	@assert(numStationSlots == length(benefitSlots))
	@assert(all(in(1:numStations), stationSlots))

	if checkMode
		if marginalBenefitsDecreasing
			for j = 1:numStations
				# check that benefit of ambulance k at station j is > benefit of ambulance k+1, otherwise marginalBenefitsDecreasing should be false
				@assert(issorted(benefitSlots[findall(stationSlots .== j)], lt=<=, rev=true))
			end
		end
	end

	# shorthand:
	a = numMoveUpAmbs
	ai = 1:numMovableAmbs # row indices of ambToStationCosts that are for movable ambs
	aj = (1:numAtHospitalAmbs).+numMovableAmbs # row indices of ambToStationCosts that are for at-hospital ambs
	s = numStations
	n = length(stationSlots) # = length(benefitSlots)

	# using JuMP
	model = Model()
	set_optimizer(model, with_optimizer(GLPK.Optimizer)) # faster without presolve; have not compared with other solvers
	# set_optimizer(model, with_optimizer(Cbc.Optimizer, logLevel=0))

	@variable(model, x[i=1:a,j=1:s], Bin)
	marginalBenefitsDecreasing ? @variable(model, 0 <= y[k=1:n] <= 1) : @variable(model, y[k=1:n], Bin)
	# x[i,j] = 1 if ambulance moveUpAmbs[i] should be moved to station j
	# sum(y[stationSlots .== j]) = number of ambulances assigned to station j

	@constraints(model, begin
		(movableAmbAtOneLocation[i=ai], sum(x[i,:]) == 1) # each movable ambulance must be assigned to one station
		(atHospitalAmbAtMostOneLocation[i=aj], sum(x[i,:]) <= 1) # each at-hospital ambulance may be assigned to zero or one station
		(stationAmbCounts[j=1:s], sum(x[:,j]) == sum(y[k] for k=findall(stationSlots .== j)))
		# (stationAmbCounts[j=1:s], sum(x[:,j]) >= sum(y[k] for k=findall(stationSlots .== j))) # should have same effect as "==" constraint (instead of ">="), but may be faster?
	end)

	if !marginalBenefitsDecreasing
		# need to enforce station slot filling order
		p = stationSlotsOrderPairs # shorthand
		@constraint(model, stationSlotsFillingOrder[k=1:size(p,1)], y[p[k,1]] >= y[p[k,2]])
	end

	@expressions(model, begin
		totalBenefitAtStations, sum(y .* benefitSlots)
		totalAmbTravelCosts, sum(x .* ambToStationCosts)
	end)

	# solve
	@objective(model, Max, totalBenefitAtStations - totalAmbTravelCosts)
	@stdout_silent optimize!(model)
	@assert(termination_status(model) == MOI.OPTIMAL)

	# get solution
	vals = Dict()
	vals[:x] = JuMP.value.(x)
	vals[:y] = JuMP.value.(y)

	# solution
	sol = convert(Array{Bool,2}, round.(vals[:x]))
	movableAmbStations = [findfirst(sol[i,:]) for i = ai]
	atHospitalAmbStations = [findfirst(sol[i,:]) for i = aj]
	atHospitalAmbStations = convert(Vector{Int}, replace(atHospitalAmbStations, nothing => nullIndex))

	if checkMode
		@assert(all(sum(sol, dims=2) .<= 1)) # each ambulance can be used in move up at most once

		# check that y values are ordered correctly
		stationSlotsFilled = convert(Vector{Bool}, round.(vals[:y]))
		for j = 1:numStations
			@assert(issorted(stationSlotsFilled[stationSlots .== j], rev=true))
		end
	end

	return movableAmbStations, atHospitalAmbStations # atHospitalAmbStations[i] == nullIndex if amb i (of those at hospital) is not moved
end

# solve as an assignment problem
# this assumes that the benefit of adding amb k to a station is > benefit of adding amb k+1
function solveZhangIpAssignmentProblem(stationSlots::Vector{Int}, benefitSlots::Vector{Float}, ambToStationCosts::Array{Float,2},
	numMovableAmbs::Int, numAtHospitalAmbs::Int)::Tuple{Vector{Int}, Vector{Int}}
	# ambToStationCosts[:,j] gives costs to move different ambulances to station j
	# ambToStationCosts[1:numMovableAmbs,:] are for ambs that can be moved now
	# ambToStationCosts[(1:numAtHospitalAmbs).+numMovableAmbs,:] are for ambs that are at hospital

	(numMoveUpAmbs, numStations) = size(ambToStationCosts)
	if numMoveUpAmbs == 0 return (Int[], Int[]) end
	@assert(numMoveUpAmbs == numMovableAmbs + numAtHospitalAmbs)
	numStationSlots = length(stationSlots)
	@assert(numStationSlots >= numMoveUpAmbs)
	@assert(numStationSlots == length(benefitSlots))
	@assert(all(in(1:numStations), stationSlots))

	if checkMode && false # skip this check, it is slow
		for j = 1:numStations
			# check that benefit of ambulance k at station j is > benefit of ambulance k+1, otherwise cannot solve as assignment problem
			@assert(issorted(benefitSlots[findall(stationSlots .== j)], lt=<=, rev=true))
		end
	end

	# shorthand
	a = numMoveUpAmbs
	ai = 1:numMovableAmbs # row indices of ambToStationCosts that are for movable ambs
	aj = (1:numAtHospitalAmbs).+numMovableAmbs # row indices of ambToStationCosts that are for at-hospital ambs
	n = numStationSlots + numAtHospitalAmbs # total number of slots, need one dummy per each amb at hospital
	si = 1:numStationSlots # column indices of ambToStationCosts that are for real stations
	sj = numStationSlots+1:n # column indices of ambToStationCosts that are for dummy stations

	# formulate assignment problem and solve
	@assert(a <= n) # needed for Hungarian algorithm
	weights = zeros(Float, a, n)
	for i = 1:a, j = si
		weights[i,j] = -(benefitSlots[j] - ambToStationCosts[i,stationSlots[j]]) # note that Hungarian algorithm looks for min cost assignment
	end
	weights[ai, sj] .= Inf # movable ambs cannot be assigned to dummy stations
	matching = Hungarian.munkres(weights) # returns a sparse matrix

	# extract solution
	I = findall(matching .== Hungarian.STAR)
	(ambIndices, stationSlotIndices) = (getindex.(I,1), getindex.(I,2))
	ambStations = zeros(Int,a) # will be the station (real or dummy) of each ambulance
	stationSlotsExtended = vcat(stationSlots, fill(nullIndex, numAtHospitalAmbs)) # add dummy station indices
	ambStations[ambIndices] = stationSlotsExtended[stationSlotIndices]
	movableAmbStations = ambStations[ai]
	atHospitalAmbStations = ambStations[aj]

	if checkMode
		@assert(!any(isequal(nullIndex), movableAmbStations)) # make sure that no movable ambs are assigned to dummy stations

		if false # skip this check, it is slow
			# check that station slots are filled in correct order
			stationSlotsFilled = fill(false, numStationSlots)
			for i in stationSlotIndices
				if i <= numStationSlots stationSlotsFilled[i] = true end
			end
			for j = 1:numStations
				@assert(issorted(stationSlotsFilled[findall(stationSlots .== j)], rev=true))
			end
		end
	end

	return movableAmbStations, atHospitalAmbStations # atHospitalAmbStations[i] == nullIndex if amb i (of those at hospital) is not moved
end


#= MIT License

Copyright (c) 2020, 2021, 2022 Uwe Fechner

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. =#

#= Model of a kite-power system in implicit form: residual = f(y, yd)

This model implements a 3D mass-spring system with reel-out. It uses six tether segments (the number can be
configured in the file data/settings.yaml). The kite is modelled as additional mass at the end of the tether.
The spring constant and the damping decrease with the segment length. The aerodynamic kite forces are
calculated, depending on reel-out speed, depower and steering settings.

One point kite model, included from KiteModels.jl.

Scientific background: http://arxiv.org/abs/1406.6218 =#

"""
    mutable struct KPS3{S, T, P} <: AbstractKiteModel

State of the kite power system. Parameters:
- S: Scalar type, e.g. SimFloat
  In the documentation mentioned as Any, but when used in this module it is always SimFloat and not Any.
- T: Vector type, e.g. MVector{3, SimFloat}
- P: number of points of the system, segments+1

Normally a user of this package will not have to access any of the members of this type directly,
use the input and output functions instead.

$(TYPEDFIELDS)
"""
@with_kw mutable struct KPS3{S, T, P} <: AbstractKiteModel
    "Reference to the settings struct"
    set::Settings = se()
    "Reference to the KCU model (Kite Control Unit as implemented in the package KitePodModels"
    kcu::KCU = KCU()
    "Reference to the atmospheric model as implemented in the package AtmosphericModels"
    am::AtmosphericModel = AtmosphericModel()
    "Reference to winch model as implemented in the package WinchModels"
    wm::AbstractWinchModel = AsyncMachine()
    "Iterations, number of calls to the function residual!"
    iter:: Int64 = 0
    "Function for calculation the lift coefficent, using a spline based on the provided value pairs."
    calc_cl = Spline1D(se().alpha_cl, se().cl_list)
    "Function for calculation the drag coefficent, using a spline based on the provided value pairs."
    calc_cd = Spline1D(se().alpha_cd, se().cd_list)
    "wind vector at the height of the kite"
    v_wind::T =           zeros(S, 3)
    "wind vector at reference height"
    v_wind_gnd::T =       zeros(S, 3)
    "wind vector used for the calculation of the tether drag"
    v_wind_tether::T =    zeros(S, 3)
    "apparent wind vector at the kite"
    v_apparent::T =       zeros(S, 3)
    "vector, perpendicular to v_apparent; output of calc_drag"
    v_app_perp::T =       zeros(S, 3)
    "drag force of kite and bridle; output of calc_aero_forces"
    drag_force::T =       zeros(S, 3)
    "lift force of the kite; output of calc_aero_forces"
    lift_force::T =       zeros(S, 3)
    "steering force acting on the kite; output of calc_aero_forces"
    steering_force::T =   zeros(S, 3)
    last_force::T =       zeros(S, 3)
    "spring force of the current tether segment, output of calc_res"
    spring_force::T =     zeros(S, 3)
    total_forces::T =     zeros(S, 3)
    "sum of spring and drag forces acting on the current segment, output of calc_res"
    force::T =            zeros(S, 3)
    "unit vector in the direction of the current tether segment, output of calc_res"
    unit_vector::T =      zeros(S, 3)
    "average velocity of the current tether segment, output of calc_res"
    av_vel::T =           zeros(S, 3)
    "y-vector of the kite fixed referense frame, output of calc_aero_forces"
    kite_y::T =           zeros(S, 3)
    "vector representing one tether segment (p1-p2)"
    segment::T =          zeros(S, 3)
    "vector of the drag force of the last calculated tether segment"
    last_tether_drag::T = zeros(S, 3)
    "z vector of the kite reference frame"
    vec_z::T =            zeros(S, 3)
    "part one of the residual, difference between pos' and vel, non-flat, mainly for unit testing"
    res1::SVector{P, KVec3} = zeros(SVector{P, KVec3})
    "part two of the residual, difference between vel' and acc, non-flat, mainly for unit testing"
    res2::SVector{P, KVec3} = zeros(SVector{P, KVec3})
    "vector of the positions of the particles"
    pos::SVector{P, KVec3} = zeros(SVector{P, KVec3})
    "velocity vector of the kite"
    vel_kite::T =          zeros(S, 3)
    "area of one tether segment [m²]"
    seg_area::S =         zero(S)
    "total actual bridle area [m²]"
    bridle_area::S =      zero(S)
    "spring constant, depending on the length of the tether segment"
    c_spring::S =         zero(S)
    "unstressed segment length [m]"
    segment_length::S =           0.0
    "damping factor, depending on the length of the tether segment"
    damping::S =          zero(S)
    "last norm of the apparent wind speed at a tether segment [m/s]"
    last_v_app_norm_tether::S = zero(S)
    "lift coefficient of the kite, depending on the angle of attack"
    param_cl::S =         0.2
    "drag coefficient of the kite, depending on the angle of attack"
    param_cd::S =         1.0
    "correction term for the steering, in paper named i_(s,c), Eq. 30"
    cor_steering::S =     zero(S)
    "azimuth angle in radian; inital value is zero"
    psi::S =              zero(S)
    "elevation angle in radian; initial value about 70 degrees"
    beta::S =             deg2rad(se().elevation)
    "depower angle [deg]"
    alpha_depower::S =     0.0
    "relative start time of the current time interval"
    t_0::S =               0.0
    "reel out speed of the winch [m/s]"
    v_reel_out::S =        0.0
    "reel out speed during the last time step"
    last_v_reel_out::S =   0.0
    "unstretched tether length"
    l_tether::S =          0.0
    "air density at the height of the kite"
    rho::S =               0.0
    "actual relative depower setting,  must be between    0 .. 1.0"
    depower::S =           0.0
    "actual relative steering setting, must be between -1.0 .. 1.0"
    steering::S =          0.0
    "factor for the tether stiffness, used to find the steady state with a low stiffness first"
    stiffness_factor::S =  1.0
    "pre-calculated constant for the wind profile law calcuation"
    log_href_over_z0::S =  log(se().h_ref / se().z0)
    "initial masses of the point masses"
    initial_masses::MVector{P, S} = ones(P)
    "current masses, depending on the total tether length"
    masses::MVector{P, S}         = ones(P)
    "synchronous speed of the motor/ generator"
    sync_speed::S =        0.0
end

"""
    clear!(s::KPS3)

Initialize the kite power model.
"""
function clear!(s::KPS3)
    s.t_0 = 0.0                              # relative start time of the current time interval
    s.v_reel_out = 0.0
    s.last_v_reel_out = 0.0
    s.v_wind_gnd    .= [s.set.v_wind, 0, 0]    # wind vector at reference height
    s.v_wind_tether .= [s.set.v_wind, 0, 0]
    s.v_apparent    .= [s.set.v_wind, 0, 0]
    height = sin(deg2rad(s.set.elevation)) * s.set.l_tether
    s.v_wind .= s.v_wind_gnd * calc_wind_factor(s.am, height)
    s.alpha_depower = 0.0
    s.l_tether = s.set.l_tether
    s.segment_length = s.l_tether / s.set.segments
    s.beta = deg2rad(s.set.elevation)
    s.rho = calc_rho(s.am, height)
    if s.set.version == 1
        # for compatibility with the python code and paper
        mass_per_meter = 0.011
    else
        mass_per_meter = s.set.rho_tether * π * (s.set.d_tether/2000.0)^2
    end
    s.initial_masses .= ones(s.set.segments+1) * mass_per_meter * s.set.l_tether / s.set.segments # Dyneema: 1.1 kg/ 100m
    s.c_spring = s.set.c_spring / s.segment_length
    s.damping  = s.set.damping / s.segment_length
    s.calc_cl = Spline1D(s.set.alpha_cl, s.set.cl_list)
    s.calc_cd = Spline1D(s.set.alpha_cd, s.set.cd_list)
    s.kcu.depower = s.set.depower/100.0
    s.kcu.set_depower = s.kcu.depower
    KiteModels.set_depower_steering!(s, get_depower(s.kcu), get_steering(s.kcu))
end

function KPS3(kcu::KCU)
    s = KPS3{SimFloat, KVec3, kcu.set.segments+1}()
    s.set = kcu.set
    s.kcu = kcu
    s.calc_cl = Spline1D(s.set.alpha_cl, s.set.cl_list)
    s.calc_cd = Spline1D(s.set.alpha_cd, s.set.cd_list)
    clear!(s)
    return s
end

"""
    calc_drag(s::KPS3, v_segment, unit_vector, rho, last_tether_drag, v_app_perp)

Calculate the drag of one tether segment, result stored in parameter `last_tether_drag`.
Return the norm of the apparent wind velocity.
"""
function calc_drag(s::KPS3, v_segment, unit_vector, rho, v_app_perp, area)
    s.v_apparent .= s.v_wind_tether - v_segment
    v_app_norm = norm(s.v_apparent)
    v_app_perp .= s.v_apparent .- s.v_apparent ⋅ unit_vector .* unit_vector
    s.last_tether_drag .= -0.5 * s.set.cd_tether * rho * norm(v_app_perp) * area .* v_app_perp
    v_app_norm
end

#     pos_kite:     position of the kite
#     rho:          air density [kg/m^3]
#     paramCD:      drag coefficient (function of power settings)
#     paramCL:      lift coefficient (function of power settings)
#     rel_steering: value between -1.0 and +1.0
function calc_aero_forces(s::KPS3, pos_kite, v_kite, rho, rel_steering)
    s.v_apparent    .= s.v_wind - v_kite
    v_app_norm       = norm(s.v_apparent)
    s.drag_force    .= s.v_apparent ./ v_app_norm
    s.kite_y        .= normalize(cross(pos_kite, s.drag_force))
    K                = 0.5 * rho * v_app_norm^2 * s.set.area
    s.lift_force    .= K * s.param_cl .* normalize(cross(s.drag_force, s.kite_y))
    # some additional drag is created while steering
    s.drag_force    .*= K * s.param_cd * BRIDLE_DRAG * (1.0 + 0.6 * abs(rel_steering))
    s.cor_steering    = s.set.c2_cor / v_app_norm * sin(s.psi) * cos(s.beta) # in paper named i_(s,c), Eq. 30
    s.steering_force .= -K * s.set.rel_side_area/100.0 * s.set.c_s * (rel_steering + s.cor_steering) .* s.kite_y
    s.last_force     .= -(s.lift_force + s.drag_force + s.steering_force)
    nothing
end

"""
    kite_ref_frame(s::KPS3)

Returns a tuple of the x, y, and z vectors of the kite reference frame.
"""
function kite_ref_frame(s::KPS3)
    pos_kite = s.pos[end]
    delta = pos_kite - s.pos[end - 1]
    c = -delta
    z = normalize(c)
    y = normalize(s.v_apparent × c)
    x = normalize(y × c)
    x, y, z
end

"""
    calc_height(s::KPS3)

Determine the height of the kite particle above ground.
"""
function calc_height(s::KPS3)
    pos_kite(s)[3]
end

"""
    pos_kite(s::KPS3)

Return the position of the kite (top particle).
"""
function pos_kite(s::KPS3)
    s.pos[end]
end

# Calculate the vector res1, that depends on the velocity and the acceleration.
# The drag force of each segment is distributed equaly on both particles.
function calc_res(s::KPS3, pos1, pos2, vel1, vel2, mass, veld, result, i)
    s.segment .= pos1 - pos2
    height = (pos1[3] + pos2[3]) * 0.5
    rho = calc_rho(s.am, height)               # calculate the air density
    rel_vel = vel1 - vel2                # calculate the relative velocity
    s.av_vel .= 0.5 * (vel1 + vel2)
    norm1 = norm(s.segment)
    s.unit_vector .= normalize(s.segment) # unit vector in the direction of the tether
    # # look at: http://en.wikipedia.org/wiki/Vector_projection
    # # calculate the relative velocity in the direction of the spring (=segment)
    spring_vel = s.unit_vector ⋅ rel_vel

    k2 = 0.05 * s.c_spring * s.stiffness_factor             # compression stiffness tether segments
    if norm1 - s.segment_length > 0.0
        s.spring_force .= (s.c_spring * s.stiffness_factor * (norm1 - s.segment_length) + s.damping * spring_vel) .* s.unit_vector
    else
        s.spring_force .= k2 * ((norm1 - s.segment_length) + (s.damping * spring_vel)) .* s.unit_vector
    end
    s.seg_area = norm1 * s.set.d_tether/1000.0
    s.last_v_app_norm_tether = calc_drag(s, s.av_vel, s.unit_vector, rho, s.v_app_perp, s.seg_area)
    s.force .= s.spring_force + 0.5 * s.last_tether_drag

    if i == s.set.segments+1 # add the drag of the bridle lines
        s.bridle_area =  s.set.l_bridle * s.set.d_line/1000.0
        s.last_v_app_norm_tether = calc_drag(s, s.av_vel, s.unit_vector, rho, s.v_app_perp, s.bridle_area)
        s.force .+= s.last_tether_drag
    end

    s.total_forces .= s.force + s.last_force
    s.last_force .= 0.5 * s.last_tether_drag - s.spring_force
    acc = s.total_forces ./ mass # create the vector of the spring acceleration
    # result .= veld - (s.acc + SVector(0,0, -G_EARTH)) # Python code, wrong
    result .= veld - (SVector(0, 0, -G_EARTH) - acc)
    nothing
end

# Calculate the vector res1 using a vector expression, and calculate res2 using a loop
# that iterates over all tether segments.
function loop(s::KPS3, pos, vel, posd, veld, res1, res2)
    s.masses               .= s.segment_length / (s.l_tether / s.set.segments) .* s.initial_masses
    s.masses[s.set.segments+1]   += (s.set.mass + s.set.kcu_mass)
    res1[1] .= pos[1]
    res2[1] .= vel[1]
    for i in 2:s.set.segments+1
        res1[i] .= vel[i] - posd[i]
    end
    for i in s.set.segments+1:-1:2
        calc_res(s, pos[i], pos[i-1], vel[i], vel[i-1], s.masses[i], veld[i],  res2[i], i)
    end
    nothing
end

"""
    winch_force(s::KPS3)

Return the absolute value of the force at the winch as calculated during the last timestep.
"""
function winch_force(s::KPS3) norm(s.last_force) end

"""
    calc_set_cl_cd!(s::KPS3, vec_c, v_app)

Calculate the lift over drag ratio as a function of the direction vector of the last tether
segment, the current depower setting and the apparent wind speed.
Set the calculated CL and CD values in the struct s.
"""
function calc_set_cl_cd!(s::KPS3, vec_c, v_app)
    s.vec_z .= normalize(vec_c)
    alpha = calc_alpha(v_app, s.vec_z) - s.alpha_depower
    set_cl_cd!(s, alpha)
end

"""
    residual!(res, yd, y::MVector{S, SimFloat}, s::KPS3, time) where S

    N-point tether model, one point kite at the top:
    Inputs:
    State vector y   = pos1, pos2, ..., posn, vel1, vel2, ..., veln
    Derivative   yd  = vel1, vel2, ..., veln, acc1, acc2, ..., accn
    Output:
    Residual     res = res1, res2 = pos1,  ..., vel1, ...

    Additional parameters:
    s: Struct with work variables, type KPS3
    S: The dimension of the state vector
The number of the point masses of the model N = S/6, the state of each point
is represented by two 3 element vectors.
"""
function residual!(res, yd, y::MVector{S, SimFloat}, s::KPS3, time) where S
    # unpack the vectors y and yd
    part = reshape(SVector{S}(y),  Size(3, div(S,6), 2))
    partd = reshape(SVector{S}(yd),  Size(3, div(S,6), 2))
    pos1, vel1 = part[:,:,1], part[:,:,2]
    pos = SVector{div(S,6)+1}(if i==1 SVector(0.0,0,0) else SVector(pos1[:,i-1]) end for i in 1:div(S,6)+1)
    vel = SVector{div(S,6)+1}(if i==1 SVector(0.0,0,0) else SVector(vel1[:,i-1]) end for i in 1:div(S,6)+1)
    posd1, veld1 = partd[:,:,1], partd[:,:,2]
    posd = SVector{div(S,6)+1}(if i==1 SVector(0.0,0,0) else SVector(posd1[:,i-1]) end for i in 1:div(S,6)+1)
    veld = SVector{div(S,6)+1}(if i==1 SVector(0.0,0,0) else SVector(veld1[:,i-1]) end for i in 1:div(S,6)+1)

    # update parameters
    pos_kite = pos[div(S,6)+1]
    s.vel_kite .= vel[div(S,6)+1]
    delta_t = time - s.t_0
    delta_v = s.v_reel_out - s.last_v_reel_out
    s.segment_length = (s.l_tether + s.last_v_reel_out * delta_t + 0.5 * delta_v * delta_t^2) / div(S,6)
    s.c_spring = s.set.c_spring / s.segment_length
    s.damping  = s.set.damping / s.segment_length
    s.beta = calc_elevation(s)

    # call core calculation routines
    vec_c = SVector{3, SimFloat}(pos[s.set.segments] - pos_kite)     # convert to SVector to avoid allocations
    v_app = SVector{3, SimFloat}(s.v_wind - s.vel_kite)
    calc_set_cl_cd!(s, vec_c, v_app)
    calc_aero_forces(s, pos_kite, s.vel_kite, s.rho, s.steering) # force at the kite
    loop(s, pos, vel, posd, veld, s.res1, s.res2)

    # copy and flatten result
    for i in 2:div(S,6)+1
        for j in 1:3
           @inbounds res[3*(i-2)+j]              = s.res1[i][j]
           @inbounds res[3*(div(S,6))+3*(i-2)+j] = s.res2[i][j]
        end
    end
    if norm(res) < 1e5
        # println(norm(res))
        for i in eachindex(pos)
            @inbounds s.pos[i] .= pos[i]
        end
    end
    # @assert ! isnan(norm(res))
    s.iter += 1
    nothing
end

# Calculate the initial conditions y0 and yd0. Tether with the initial elevation angle
# se().elevation, particle zero fixed at origin.
# Parameters:
# x: vector of deviations of the tether particle positions from a straight line in x and z
# length(x) == 2*SEGMENTS
# Returns:
# res, a single vector consisting of the elements of y0 and yd0
function init_inner(s::KPS3, X=zeros(2 * s.set.segments); old=false, delta=0.0)
    pos = zeros(SVector{s.set.segments+1, KVec3})
    vel = zeros(SVector{s.set.segments+1, KVec3})
    acc = zeros(SVector{s.set.segments+1, KVec3})
    state_y0 = zeros(SVector{2*s.set.segments, KVec3})
    yd0 = zeros(SVector{2*s.set.segments, KVec3})

    DELTA = delta
    set_cl_cd!(s, 10.0/180.0 * π)

    for i in 0:s.set.segments
        radius =  -i * s.set.l_tether / s.set.segments
        elevation = s.set.elevation
        sin_el, cos_el = sin(elevation / 180.0 * π), cos(elevation / 180.0 * π)
        if i == 0
            pos[i+1] .= SVec3(0.0, DELTA, 0.0)
        else
            pos[i+1] .= SVec3(-cos_el * radius+X[i], DELTA, -sin_el * radius+X[s.set.segments+i])
        end
        vel[i+1] .= SVec3(DELTA, DELTA, DELTA)
        acc[i+1] .= SVec3(DELTA, DELTA, DELTA)
    end
    for i in eachindex(pos)
        @inbounds s.pos[i] .= pos[i]
    end

    for i in 2:s.set.segments+1
        state_y0[i-1] .= pos[i]  # Initial state vector
        yd0[i-1]      .= vel[i]  # Initial state vector derivative
    end

    for i in 2:s.set.segments+1
        state_y0[s.set.segments+i-1] .= vel[i]  # Initial state vector
        yd0[s.set.segments+i-1]      .= acc[i]  # Initial state vector derivative
    end
    set_v_wind_ground!(s, pos[s.set.segments+1][3])
    s.l_tether = s.set.l_tether
    set_v_reel_out!(s, s.set.v_reel_out, 0.0)

    state_y0, yd0
end

# same as above, but returns a tuple of two one dimensional arrays
function init(s::KPS3, X=zeros(2 * (s.set.segments)); old=false, delta = 0.0)
    res1_, res2_ = init_inner(s, X; old=old, delta = delta)
    res1, res2  = reduce(vcat, res1_), reduce(vcat, res2_)
    MVector{6*(s.set.segments), Float64}(res1), MVector{6*(s.set.segments), Float64}(res2)
end

"""
    spring_forces(s::AKM)

Return an array of the scalar spring forces of all tether segements.
"""
function spring_forces(s::KPS3)
    forces = zeros(SimFloat, s.set.segments)
    for i in 1:s.set.segments
        forces[i] =  s.c_spring * s.stiffness_factor * (norm(s.pos[i+1] - s.pos[i]) - s.segment_length)
    end
    forces
end

function find_steady_state_inner(s::KPS3, X, prn=false; delta=0.0)
    res = zeros(MVector{6*s.set.segments, SimFloat})

    # helper function for the steady state finder
    function test_initial_condition!(F, x::Vector)
        y0, yd0 = init(s, x, delta=delta)
        residual!(res, yd0, y0, s, 0.0)
        for i in 1:s.set.segments
            F[i]                = res[1 + 3*(i-1) + 3*s.set.segments]
            F[i+s.set.segments] = res[3 + 3*(i-1) + 3*s.set.segments]
        end
        return nothing
    end

    if prn println("\nStarted function test_nlsolve...") end
    results = nlsolve(test_initial_condition!, X, xtol=1e-6, ftol=1e-6, autoscale=true, iterations=1000)
    if prn println("\nresult: $results") end
    results.zero
 end

"""
    find_steady_state!(s::KPS3, prn=false, delta = 0.0, stiffness_factor=0.035)

Find an initial equilibrium, based on the inital parameters
`l_tether`, elevation and `v_reel_out`.
"""
function find_steady_state!(s::KPS3; prn=false, delta = 0.0, stiffness_factor=0.035)
    zero = zeros(SimFloat, 2*s.set.segments)
    s.stiffness_factor=stiffness_factor
    zero = find_steady_state_inner(s, zero, prn, delta=delta)
    s.stiffness_factor=1.0
    zero = find_steady_state_inner(s, zero, prn, delta=delta)
    init(s, zero; delta=delta)
end


module TestUtil

using FactCheck

importall Orchestra.Util

include("fixture_learners.jl")
using .FixtureLearners
nfcp = NumericFeatureClassification()

facts("Orchestra util functions") do
  context("holdout returns proportional partitions") do
    n = 10
    right_prop = 0.3
    (left, right) = holdout(n, right_prop)

    @fact size(left, 1) => n - (n * right_prop)
    @fact size(right, 1) => n * right_prop
    @fact intersect(left, right) => isempty
    @fact size(union(left, right), 1) => n
  end

  context("kfold returns k partitions") do
    num_instances = 10
    num_partitions = 3
    partitions = kfold(num_instances, num_partitions)

    @fact size(partitions, 1) => num_partitions
    [@fact length(partition) >= 6 => true for partition in partitions]
  end

  context("score calculates accuracy") do
    learner = PerfectScoreLearner({:problem => nfcp})
    predictions = fit_and_transform!(learner, nfcp)

    @fact score(
      :accuracy, nfcp.test_labels, predictions
    ) => 100.0
  end
  context("score throws exception on unknown metric") do
    learner = PerfectScoreLearner({:problem => nfcp})
    predictions = fit_and_transform!(learner, nfcp)

    @fact_throws score(
      :fake, nfcp.test_labels, predictions
    )
  end

  context("infer_eltype returns inferred elements type") do
    vector = [1,2,3,"a"]
    @fact infer_eltype(vector[1:3]) => Int
  end

  context("nested_dict_to_tuples produces list of tuples") do
    nested_dict = {
      :a => [1,2],
      :b => {
        :c => [3,4,5]
      }
    }
    expected_set = Set({
      ([:a], [1,2]),
      ([:b,:c], [3,4,5])
    })
    set = nested_dict_to_tuples(nested_dict)

    @fact set => expected_set
  end

  context("nested_dict_set! assigns values") do
    nested_dict = {
      :a => 1,
      :b => {
        :c => 2
      }
    }
    expected_dict = {
      :a => 1,
      :b => {
        :c => 3
      }
    }
    nested_dict_set!(nested_dict, [:b,:c], 3)

    @fact nested_dict => expected_dict
  end

  context("nested_dict_merge merges two nested dictionaries") do
    first = {
      :a => 1,
      :b => {
        :c => 2,
        :d => 3
      }
    }
    second = {
      :a => 4,
      :b => {
        :d => 5
      }
    }
    expected = {
      :a => 4,
      :b => {
        :c => 2,
        :d => 5
      }
    }
    actual = nested_dict_merge(first, second)

    @fact actual => expected
  end

  context("create_transformer produces new transformer") do
    learner = AlwaysSameLabelLearner({:label => :a})
    new_options = {:label => :b}
    new_learner = create_transformer(learner, new_options)

    @fact learner.options[:label] => :a
    @fact new_learner.options[:label] => :b
    @fact true => !isequal(learner, new_learner)
  end
end

end # module


module VoronoiCells

# We do not compile
# 1. VoronoiDelaunay is not compiled.
# 2. VoronoiCells itself loads in about 100ms

include("voronoi_cells.jl")
include("store_cells.jl")

end # module


module ModelTools
using SemanticModels.Parsers

export model, callsites, structured, AbstractModel,
    pusharg!, setarg!, bodyblock, argslist, issome,
    head, isblock, isfunc, or, and, isexpr, iscall,
    isusing, isimport, funcarg, funcname,
    Edge, Edges, typegraph, @typegraph

# TODO Possible imports/exports: invoke

""" isexpr(x)

predicate for isa(x, Expr).
"""
isexpr(x) = isa(x, Expr)

"""    AbstractModel

a placeholder struct to dispatch on how to parse the expression tree into a model.
"""
abstract type AbstractModel end

""" model(::AbstractModel, expr::Expr)

dig into the expression that describes a model and break it down into components. This allows you to construct a structured representation of the modeling problem at the expression level. Just like how julia modeling frameworks build structured representations of the problems in data structures. This version builds them at the expression level.

The first argument is the type you want to construct, the second argument is the expression that you want to analyze. For example

```
model(ExpODEModel, expr)::ExpODEModel
```

"""
function model(::Type{T}, expr::Expr) where T<:AbstractModel
    error("NotImplemented: model(::$T,::Expr")
end

function invoke(m::AbstractModel, args...)
    Mod = eval(m.expr)
    Base.invokelatest(Mod.main, args...)
end

"""    callsites(expr::Expr, name::Symbol)

extract the location where the function `name` is called in `expr`.
"""
function callsites(expr::Expr, name::Symbol)
    matches = Expr[]
    f(x::Any) = x
    f(x::Expr) = begin
        if x.head == :call && x.args[1] == name
            push!(matches, x)
            return x
        else
            return walk(x, f, g)
        end
    end
    g(x) = x
    walk(expr, f, g)
    return matches
end

"""    structured(func, var::Symbol, assign=true)

extract the expressions that use structuring/destructuring assignment
to name the components of `var`
"""
function structured(func::Expr, var::Symbol, assign=true)
    body = func.args[2]
    paramvec = Expr[]
    for line in body.args
        if isa(line, LineNumberNode)
            continue
        end
        if  assign && line.head == :(=) && inexpr(line.args[2], var)
            push!(paramvec, line)
        end
        if !assign && line.head == :(=) && inexpr(line.args[1], var)
            push!(paramvec, line)
        end
    end
    return paramvec
end

lhs(x::Expr) = begin
    @show x
    x.head == :(=) || error("x is not an assignment")
    return x.args[1]
end

"""    bodyblock(expr::Expr)

get the array of args representing the body of a function definition.
"""
function bodyblock(expr::Expr)
    expr.head == :function || error("$expr is not a function definition")
    return expr.args[2].args
end

"""    funclines(expr::Expr, s::Symbol)

clean up the lines of a function definition for presentation
"""
function funclines(expr::Expr, s::Symbol)
    q = Expr(:block)
    q.args = (filter(isexpr, findfunc(expr, s))[end]
              |> bodyblock
              |> arr -> filter(x->!isa(x, LineNumberNode),arr))
    return q
end

"""    argslist(expr::Expr)

get the array of args representing the arguments of a defined function.
the first element of this list is the function name

See also [`bodyblock`](@ref), [`pusharg!`](@ref),
"""
function argslist(expr::Expr)
    expr.head == :function || error("$expr is not a function definition")
    return expr.args[1].args
end

"""    pusharg!(expr::Expr, s::Symbol)

push a new argument onto the definition of a function.

See also [`argslist`](@ref), [`setarg!`](@ref)
"""
function pusharg!(ex::Expr, s::Symbol)
    ex.head == :function || error("ex is not a function definition")
    push!(argslist(ex), s)
    return ex
end

"""    setarg!(expr::Expr, s::Symbol)

replace the argument in a function call.

See also [`argslist`](@ref), [`pusharg!`](@ref)
"""
function setarg!(ex::Expr, old, new)
    ex.head == :call || error("ex is not a function call")
    for (i, x) in enumerate(ex.args)
        if x == old
            ex.args[i] = new
        end
    end
    return ex
end

function funcarg(ex::Expr)
    return ex.args[1].args[2]
end

include("exprs.jl")
include("typegraphs.jl")
include("Transformations.jl")
include("SimpleModels.jl")
include("ExpODEModels.jl")
include("ExpStateModels.jl")
include("MonomialRegressionModels.jl")
include("CategoryTheory.jl")
include("PetriModels.jl")
include("OpenModels.jl")
include("OpenPetris.jl")
include("WiringDiagrams.jl")

end