#=
Copyright (c) 2015, Intel Corporation

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice,
      this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of Intel Corporation nor the names of its contributors
      may be used to endorse or promote products derived from this software
      without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
=#

include("../../src/Sparso.jl")
include("../../src/simple-show.jl")
using Sparso

set_options(SA_ENABLE, SA_VERBOSE, SA_USE_SPMP, SA_CONTEXT, SA_REPLACE_CALLS)#, SA_DISABLE_COLLECTIVE_STRUCTURE_PREDICTION)

include("./ipm-ref.jl")
include("utils.jl")

if length(ARGS) == 0
  A, b, p = load_ipm_input("ipm/mps/osa-14")
else
  A, b, p = load_ipm_input(ARGS[1])
end

m = size(A, 1)
n = size(A, 2)
println("Problem size = [$m $n]")
#println("\tsum of A=", sum(A))
#println("\tsum of b=", sum(b))
#println("\tsum of p=", sum(p))

println("Original: ")
x, ref_total_time, spgemm_time, fact_time, blas1_time, trslv_time, spmv_time,
    iter, relResidual, objval = ipm_ref(A, b, p)
x, ref_total_time, spgemm_time, fact_time, blas1_time, trslv_time, spmv_time,
    iter, relResidual, objval = ipm_ref(A, b, p)
@printf "\nOriginal ref_total_time = %f\n" ref_total_time
@printf "Original spgemm = %f fact = %f blas1 = %f trslv = %f spmv = %f\n" spgemm_time fact_time blas1_time trslv_time spmv_time
@printf "Original iter %2i, resid = %9.2e, objval = %e\n" iter relResidual objval

println("\n\nWithout context-sensitive optimization: ")
Sparso.set_reuse_inspection(false)
@acc x, ref_total_time, spgemm_time, fact_time, blas1_time, trslv_time, spmv_time,
    iter, relResidual, objval = ipm_ref(A, b, p)
Sparso.set_knob_log_level(1)
@acc x, ref_total_time, spgemm_time, fact_time, blas1_time, trslv_time, spmv_time,
    iter, relResidual, objval = ipm_ref(A, b, p)
@printf "\nAccelerated acc_total_time = %f\n" ref_total_time
@printf "Accelerated spgemm = %f fact = %f blas1 = %f trslv = %f spmv = %f\n" spgemm_time fact_time blas1_time trslv_time spmv_time
@printf "Accelerated iter %2i, resid = %9.2e, objval = %e\n" iter relResidual objval
Sparso.set_reuse_inspection(true)

println("\n\nAccelerated: ")
@acc x, ref_total_time, spgemm_time, fact_time, blas1_time, trslv_time, spmv_time,
    iter, relResidual, objval = ipm_ref(A, b, p)
Sparso.set_knob_log_level(1)
@acc x, ref_total_time, spgemm_time, fact_time, blas1_time, trslv_time, spmv_time,
    iter, relResidual, objval = ipm_ref(A, b, p)
@printf "\nAccelerated acc_total_time = %f\n" ref_total_time
@printf "Accelerated spgemm = %f fact = %f blas1 = %f trslv = %f spmv = %f\n" spgemm_time fact_time blas1_time trslv_time spmv_time
@printf "Accelerated iter %2i, resid = %9.2e, objval = %e\n" iter relResidual objval

#println("\n\nWith manual context-sensitive optimization: ")
#ipm_ref_simplified_with_context_opt(A, b, p)
#println("\tsum of x=", sum(x))


using Printf
using Dates: AbstractTime

maybe_int(t) = isinteger(t) ? Int(t) : t

"""
    prettytime(t)

Convert a floating point value `t` representing an amount of time in seconds to a more
human-friendly formatted string with three decimal places. Depending on the value of `t`
the string will be formatted to show `t` in nanoseconds (ns), microseconds (μs),
milliseconds (ms), seconds, minutes, hours, days, or years.
"""
function prettytime(t)
    # Modified from: https://github.com/JuliaCI/BenchmarkTools.jl/blob/master/src/trials.jl
    iszero(t) && return "0 seconds"

    t = maybe_int(t)

    if t < 1e-9
        # No point going to picoseconds, just return something readable.
        return @sprintf("%.3e seconds", t)
    elseif t < 1e-6
        value, units = t * 1e9, "ns"
    elseif t < 1e-3
        value, units = t * 1e6, "μs"
    elseif t < 1
        value, units = t * 1e3, "ms"
    elseif t < minute
        value = t
        units = value == 1 ? "second" : "seconds"
    elseif t < hour
        value = maybe_int(t / minute)
        units = value == 1 ? "minute" : "minutes"
    elseif t < day
        value = maybe_int(t / hour)
        units = value == 1 ? "hour" : "hours"
    elseif t < year
        value = maybe_int(t / day)
        units = value == 1 ? "day" : "days"
    else
        value = maybe_int(t / year)
        units = value == 1 ? "year" : "years"
    end

    if isinteger(value)
        return @sprintf("%d %s", value, units)
    else
        return @sprintf("%.3f %s", value, units)
    end
end

prettytime(dt::AbstractTime) = "$dt"


using ChainRulesCore

function ChainRulesCore.ProjectTo(x::Union{KaNda, NdaKa})
    return ProjectTo{KeyedArray}(;data=ProjectTo(keyless_unname(x)), keys=named_axiskeys(x))
end

function ChainRulesCore.ProjectTo(x::KeyedArray)
    return ProjectTo{KeyedArray}(;data=ProjectTo(keyless(x)), keys=axiskeys(x))
end

(project::ProjectTo{KeyedArray})(dx) = wrapdims(project.data(parent(dx)), project.keys...)

_KeyedArray_pullback(ȳ, project) = (NoTangent(), project(ȳ))
_KeyedArray_pullback(ȳ::Tangent, project) = _KeyedArray_pullback(ȳ.data, project)
_KeyedArray_pullback(ȳ::AbstractThunk, project) = _KeyedArray_pullback(unthunk(ȳ), project)

function ChainRulesCore.rrule(::typeof(keyless_unname), x)
    pb(y) = _KeyedArray_pullback(y, ProjectTo(x))
    return keyless_unname(x), pb
end


module Simulations

    using ProgressMeter

    using ..CommonFunctions
    using ..Equations
    using ..Integrators
    using ..Solutions

    import ..Solutions: DEFAULT_NSAVE, DEFAULT_NWRITE

    export Simulation, ParallelSimulation
    export run!

    include("simulations/simulation.jl")
    include("simulations/parallel_simulation.jl")

end


export predict!,backward!,add_layer!,add_optimizer!,get_layers,get_params,get_loss,get_now_epoch,save_model,load_model,reset!,convert_to_array,convert_to_cu,get_optimizer
#===
共通型，関数
・全てのモデルはModelCommonをcommonに持ち，Modelsの子型になる
===#

mutable struct ModelCommon
    layers
    optimizer
    epoch
    now_epoch
    min_epoch
    learn_io
    learn_plot
    loss
    min_loss
    std_params #Dict("name"=>[avg,variance,std])
    norm_params #Dict("name"=>[max,min])
    gpu_flg

    function ModelCommon(gpu_flg)
        new(
            nothing,
            nothing,
            0,
            0,
            0,
            IOBuffer(),
            nothing,
            [],
            0.0,
            Dict(),
            Dict(),
            gpu_flg
        )
    end
end

#=========
dataは全て{N,D}の形でここに渡す.
cuは引数の前にしておく
=========#
function predict!(model::Models,data,learn_flg=false)
    out = data

    @inbounds @simd for i in 1:length(model.common.layers)
        layer = get_layers(model,i)
        out = forward!(layer,out,learn_flg)
    end

    return out
end

function backward!(model::Models)
    dout = 0
    @inbounds @simd for layer in reverse(model.common.layers)
        dout = backward!(layer,dout)
    end
end

function add_layer!(model::Models,layers)
    if model.common.gpu_flg
        @inbounds for i in 1:length(layers)
            convert_to_cu!(layers[i])
        end
    end
    model.common.layers = layers
    nothing
end

function add_optimizer!(model::Models,optimizer)
    fit!(optimizer,model)
end

function reset!(model::Models)
    @inbounds @simd for i in 1:length(model.common.layers)
        reset!(model.common.layers[i])
    end
end

function convert_to_cu!(model::Models)
    #optimizer
    convert_to_cu!(get_optimizer(model),model)
    #レイヤ
    @inbounds for i in 1:length(model.common.layers)
        convert_to_cu!(model.common.layers[i])
    end
end
function convert_to_array!(model::Models)
    #optimizer
    convert_to_array!(get_optimizer(model),model)
    #レイヤ
    @inbounds for i in 1:length(model.common.layers)
        convert_to_array!(model.common.layers[i])
    end
end

function get_layers(model::Models,n=nothing)
    (n===nothing) ? model.common.layers : model.common.layers[n]
end
function get_now_epoch(model::Models)
    model.common.now_epoch
end
function get_loss(model::Models)
    model.common.loss
end
function get_params(model::Models,n=nothing)
    if n===nothing
        re = []
        @inbounds for i in 1:length(model.common.layers)
            @simd for j in 1:length(model.common.layers[i].params)
                append!(re,[model.common.layers[i].params[j]])
            end
        end
        return re
    else
        return model.common.layers[n].params
    end
end
function get_optimizer(model::Models)
    return model.common.optimizer
end

function save_model(model::Models,path)
    #パラメータ全てをArrayに直す必要がある
    if model.common.gpu_flg
        convert_to_array!(model)
    end
    save(path,"model",Dict("content"=>model))
    #GPUへ復元
    if model.common.gpu_flg
        convert_to_cu!(model)
    end
    nothing
end
function load_model(path,gpu_flg=false)
    d = load(path)
    model = d["model"]["content"]
    if gpu_flg
        convert_to_cu!(model)
    end
    return model
end

#===
ファイルの読み込み
===#
include("sequential.jl")


using Base.Test, Logging
Logging.configure(level = Logging.WARNING) # set to DEBUG to see error tables

@time @testset "DeformableRegistration" begin

@testset "submodules" begin
    include("ImageProcessing.jl")
    include("Transformation.jl")
    include("Interpolation.jl")
    include("Distance.jl")
    include("Regularizer.jl")
end

# test full parametric and nonparametric image registration
@testset "example registration cases" begin
    include("ParametricRegistration.jl")
    include("NonparametricRegistration.jl")
end

end


module UnaryOps
    using ..SuiteSparseGraphBLAS: isGxB, isGrB, TypedUnaryOperator, AbstractUnaryOp
    using ..libgb
    export UnaryOp
    function UnaryOp(name)
        if isGxB(name) || isGrB(name) #If it's a GrB/GxB op we don't want the prefix
            simplifiedname = name[5:end]
        else
            simplifiedname = name
        end
        tname = Symbol(simplifiedname * "_T")
        simplifiedname = Symbol(simplifiedname)
        structquote = quote
            struct $tname <: AbstractUnaryOp
                typedops::Dict{DataType, TypedUnaryOperator}
                name::String
                $tname() = new(Dict{DataType, TypedUnaryOperator}(), $name)
            end
        end
        @eval($structquote) #Eval the struct into the Types submodule to avoid clutter.
        constquote = quote
            const $simplifiedname = $tname()
            export $simplifiedname
        end
        @eval($constquote)
        return getproperty(UnaryOps, simplifiedname)
    end

    struct GenericUnaryOp <: AbstractUnaryOp
        typedops::Dict{DataType, TypedUnaryOperator}
        name::String
        GenericUnaryOp(name) = new(Dict{DataType, TypedUnaryOperator}(), name)
    end
end

const UnaryUnion = Union{AbstractUnaryOp, TypedUnaryOperator}

#TODO: Rewrite
function _createunaryops()
    builtins = [
    "GrB_IDENTITY",
    "GrB_AINV",
    "GxB_LNOT",
    "GrB_MINV",
    "GxB_ONE",
    "GrB_ABS",
    "GrB_BNOT",
    "GxB_SQRT",
    "GxB_LOG",
    "GxB_EXP",
    "GxB_LOG2",
    "GxB_SIN",
    "GxB_COS",
    "GxB_TAN",
    "GxB_ACOS",
    "GxB_ASIN",
    "GxB_ATAN",
    "GxB_SINH",
    "GxB_COSH",
    "GxB_TANH",
    "GxB_ASINH",
    "GxB_ACOSH",
    "GxB_ATANH",
    "GxB_SIGNUM",
    "GxB_CEIL",
    "GxB_FLOOR",
    "GxB_ROUND",
    "GxB_TRUNC",
    "GxB_EXP2",
    "GxB_EXPM1",
    "GxB_LOG10",
    "GxB_LOG1P",
    "GxB_LGAMMA",
    "GxB_TGAMMA",
    "GxB_ERF",
    "GxB_ERFC",
    "GxB_FREXPE",
    "GxB_FREXPX",
    "GxB_CONJ",
    "GxB_CREAL",
    "GxB_CIMAG",
    "GxB_CARG",
    "GxB_ISINF",
    "GxB_ISNAN",
    "GxB_ISFINITE",
    "GxB_POSITIONI",
    "GxB_POSITIONI1",
    "GxB_POSITIONJ",
    "GxB_POSITIONJ1",
]
    for name ∈ builtins
        UnaryOps.UnaryOp(name)
    end
end

#This is adapted from the fork by cvdlab.
#Add a new GrB_UnaryOp to an AbstractUnaryOp.
function _addunaryop(op::AbstractUnaryOp, fn::Function, ztype::GBType{T}, xtype::GBType{U}) where {T, U}
    function unaryopfn(z, x)
        unsafe_store!(z, fn(x))
        return nothing
    end
    opref = Ref{libgb.GrB_UnaryOp}()
    unaryopfn_C = @cfunction($unaryopfn, Cvoid, (Ptr{T}, Ref{U}))
    libgb.GB_UnaryOp_new(opref, unaryopfn_C, ztype, xtype, op.name)
    op.typedops[U] = TypedUnaryOperator{xtype, ztype}(opref[])
    return nothing
end

#UnaryOp constructors
#####################
function UnaryOps.UnaryOp(name::String, fn::Function, ztype, xtype; keep=false)
    @warn "Use built-in functions where possible, user defined functions are less performant."
    length(name) == 0 && (name = string(fn))
    if keep
        op = UnaryOps.UnaryOp(name)
    else
        op = UnaryOps.GenericUnaryOp(name)
    end
    _addunaryop(op, fn, toGBType(ztype), toGBType(xtype))
    return op
end
#Same xtype, ztype.
function UnaryOps.UnaryOp(name::String, fn::Function, type; keep=false)
    return UnaryOps.UnaryOp(name, fn, type, type; keep)
end
#Vector of xtypes and ztypes, add a GrB_UnaryOp for each.
function UnaryOps.UnaryOp(name::String, fn::Function, ztype::Vector{DataType}, xtype::Vector{DataType}; keep=false)
    @warn "Use built-in functions where possible, user defined functions are less performant."
    length(name) == 0 && (name = string(fn))
    if keep
        op = UnaryOps.UnaryOp(name)
    else
        op = UnaryOps.GenericUnaryOp(name)
    end
    length(ztype) == length(xtype) || throw(DimensionMismatch("Lengths of ztype and xtype must match."))
    for i ∈ 1:length(ztype)
        _addunaryop(op, fn, toGBType(ztype[i]), toGBType(xtype[i]))
    end
    return op
end
#Vector but same ztype xtype.
function UnaryOps.UnaryOp(name::String, fn::Function, type::Vector{DataType}; keep=false)
    return UnaryOps.UnaryOp(name, fn, type, type; keep)
end
#Construct it using all the built in primitives.
function UnaryOps.UnaryOp(name::String, fn::Function; keep=false)
    return UnaryOps.UnaryOp(name, fn, valid_vec; keep)
end

function UnaryOps.UnaryOp(fn::Function; keep=false)
    return UnaryOps.UnaryOp("", fn; keep)
end

function _load(unaryop::AbstractUnaryOp)
    booleans = ["GrB_IDENTITY", "GrB_AINV", "GrB_MINV", "GxB_LNOT", "GxB_ONE", "GrB_ABS"]
    integers = [
        "GrB_IDENTITY",
        "GrB_AINV",
        "GrB_MINV",
        "GxB_LNOT",
        "GxB_ONE",
        "GrB_ABS",
        "GrB_BNOT",
    ]
    unsignedintegers = [
        "GrB_IDENTITY",
        "GrB_AINV",
        "GrB_MINV",
        "GxB_LNOT",
        "GxB_ONE",
        "GrB_ABS",
        "GrB_BNOT",
    ]
    floats = [
        "GrB_IDENTITY",
        "GrB_AINV",
        "GrB_MINV",
        "GxB_LNOT",
        "GxB_ONE",
        "GrB_ABS",
        "GxB_SQRT",
        "GxB_LOG",
        "GxB_EXP",
        "GxB_LOG2",
        "GxB_SIN",
        "GxB_COS",
        "GxB_TAN",
        "GxB_ACOS",
        "GxB_ASIN",
        "GxB_ATAN",
        "GxB_SINH",
        "GxB_COSH",
        "GxB_TANH",
        "GxB_ASINH",
        "GxB_ACOSH",
        "GxB_ATANH",
        "GxB_SIGNUM",
        "GxB_CEIL",
        "GxB_FLOOR",
        "GxB_ROUND",
        "GxB_TRUNC",
        "GxB_EXP2",
        "GxB_EXPM1",
        "GxB_LOG10",
        "GxB_LOG1P",
        "GxB_LGAMMA",
        "GxB_TGAMMA",
        "GxB_ERF",
        "GxB_ERFC",
        "GxB_FREXPE",
        "GxB_FREXPX",
        "GxB_ISINF",
        "GxB_ISNAN",
        "GxB_ISFINITE",
    ]
    positionals = ["GxB_POSITIONI", "GxB_POSITIONI1", "GxB_POSITIONJ", "GxB_POSITIONJ1"]
    complexes = [
        "GxB_IDENTITY",
        "GxB_AINV",
        "GxB_MINV",
        "GxB_ONE",
        "GxB_SQRT",
        "GxB_LOG",
        "GxB_EXP",
        "GxB_LOG2",
        "GxB_SIN",
        "GxB_COS",
        "GxB_TAN",
        "GxB_ACOS",
        "GxB_ASIN",
        "GxB_ATAN",
        "GxB_SINH",
        "GxB_COSH",
        "GxB_TANH",
        "GxB_ASINH",
        "GxB_ACOSH",
        "GxB_ATANH",
        "GxB_SIGNUM",
        "GxB_CEIL",
        "GxB_FLOOR",
        "GxB_ROUND",
        "GxB_TRUNC",
        "GxB_EXP2",
        "GxB_EXPM1",
        "GxB_LOG10",
        "GxB_LOG1P",
        "GxB_CONJ",
        "GxB_CREAL",
        "GxB_CIMAG",
        "GxB_CARG",
        "GxB_ABS",
        "GxB_ISINF",
        "GxB_ISNAN",
        "GxB_ISFINITE",
    ]
    name = unaryop.name
    if name ∈ booleans
        constname = name * "_BOOL"
        unaryop.typedops[Bool] = TypedUnaryOperator(load_global(constname, libgb.GrB_UnaryOp))
    end

    if name ∈ integers
        unaryop.typedops[Int8] = TypedUnaryOperator(load_global(name * "_INT8", libgb.GrB_UnaryOp))
        unaryop.typedops[Int16] = TypedUnaryOperator(load_global(name * "_INT16", libgb.GrB_UnaryOp))
        unaryop.typedops[Int32] = TypedUnaryOperator(load_global(name * "_INT32", libgb.GrB_UnaryOp))
        unaryop.typedops[Int64] = TypedUnaryOperator(load_global(name * "_INT64", libgb.GrB_UnaryOp))
    end

    if name ∈ unsignedintegers
        unaryop.typedops[UInt8] = TypedUnaryOperator(load_global(name * "_UINT8", libgb.GrB_UnaryOp))
        unaryop.typedops[UInt16] = TypedUnaryOperator(load_global(name * "_UINT16", libgb.GrB_UnaryOp))
        unaryop.typedops[UInt32] = TypedUnaryOperator(load_global(name * "_UINT32", libgb.GrB_UnaryOp))
        unaryop.typedops[UInt64] = TypedUnaryOperator(load_global(name * "_UINT64", libgb.GrB_UnaryOp))
    end

    if name ∈ floats
        unaryop.typedops[Float32] = TypedUnaryOperator(load_global(name * "_FP32", libgb.GrB_UnaryOp))
        unaryop.typedops[Float64] = TypedUnaryOperator(load_global(name * "_FP64", libgb.GrB_UnaryOp))
    end
    if name ∈ positionals
        unaryop.typedops[Any] = TypedUnaryOperator(load_global(name * "_INT64", libgb.GrB_UnaryOp))
    end
    name = "GxB_" * name[5:end]
    if name ∈ complexes
        unaryop.typedops[ComplexF32] = TypedUnaryOperator(load_global(name * "_FC32", libgb.GrB_UnaryOp))
        unaryop.typedops[ComplexF64] = TypedUnaryOperator(load_global(name * "_FC64", libgb.GrB_UnaryOp))
    end
end

ztype(::TypedUnaryOperator{I, O}) where {I, O} = O
xtype(::TypedUnaryOperator{I, O}) where {I, O} = I


# This code was originally taken and modifed from LineSearches.jl for the sake of
# improving performance. Attribution / credit belongs to the original authors.
# This was the attached liscense agreement:
#
# The original code has been separated out from the Optim.jl package,
# licensed under the MIT License:
# > Copyright (c) 2012: John Myles White and other contributors.
#
# The LineSearches.jl package is licensed under the MIT "Expat" License:
#
# > Copyright (c) 2016: Asbjørn Nilsen Riseth and other contributors.
# >
# > 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.

@with_kw struct BackTracking{TF, TI}
    c_1::TF = 1e-4
    ρ_hi::TF = 0.5
    ρ_lo::TF = 0.1
    iterations::TI = 1_000
    order::TI = 3
    maxstep::TF = Inf
end
BackTracking{TF}(args...; kwargs...) where TF = BackTracking{TF,Int}(args...; kwargs...)

function linesearch!(ls::BackTracking, df, x::AbstractArray{T}, s::AbstractArray{T},
                            α_0::Tα, x_new::AbstractArray{T}, ϕ_0, dϕ_0) where {T, Tα}

    alphamax = convert(real(T), Inf)
    # ϕ, dϕ = make_ϕ_dϕ(df, x_new, x, s)

    # α_0 = min(α_0, min(alphamax, ls.maxstep / norm(s, Inf)))
    α_0 = min(α_0, min(alphamax, ls.maxstep / SIMDArrays.maximum_abs(s)))
    # ls(ϕ, α_0, ϕ_0, dϕ_0)

    c_1, ρ_hi, ρ_lo, iterations, order = ls.c_1, ls.ρ_hi, ls.ρ_lo, ls.iterations, ls.order
    # @unpack c_1, ρ_hi, ρ_lo, iterations, order = ls

    iterfinitemax = -log2(eps(real(Tα)))

    # @assert order in (2,3)
    # Check the input is valid, and modify otherwise
    #backtrack_condition = 1.0 - 1.0/(2*ρ) # want guaranteed backtrack factor
    #if c_1 >= backtrack_condition
    #    warn("""The Armijo constant c_1 is too large; replacing it with
    #                   $(backtrack_condition)""")
    #   c_1 = backtrack_condition
    #end

    # Count the total number of iterations
    iteration = 0

    ϕx_0, ϕx_1 = ϕ_0, ϕ_0

    α_1, α_2 = α_0, α_0

    @fastmath @inbounds @simd for i ∈ 1:full_length(x_new)
        x_new[i] = x[i] + α_1 * s[i]
    end
    # Evaluate f(x+α*s)

    ϕx_1 = value!(df, x_new)
    # @show ϕx_1, α_1

    # Hard-coded backtrack until we find a finite function value
    iterfinite = 0
    while !isfinite(ϕx_1) && iterfinite < iterfinitemax
        iterfinite += 1
        α_1 = α_2
        α_2 = 0.5α_1

        @fastmath @inbounds @simd for i ∈ 1:full_length(x_new)
            x_new[i] = x[i] + α_2 * s[i]
        end
        # Evaluate f(x+α*s)

        ϕx_1 = value!(df, x_new)
        # @show ϕx_1, α_2
    end

    # Backtrack until we satisfy sufficient decrease condition
    while ϕx_1 > ϕ_0 + c_1 * α_2 * dϕ_0
        # Increment the number of steps we've had to perform
        iteration += 1

        # Ensure termination
        # if iteration > iterations
        #     throw(LineSearchException("Linesearch failed to converge, reached maximum iterations $(iterations).",
        #                               α_2))=
        # end
        # iteration > iterations && throw("Exceeded maximum number of iterations.")
        # Shrink proposed step-size:
        if order == 2 || iteration == 1
            # backtracking via quadratic interpolation:
            # This interpolates the available data
            #    f(0), f'(0), f(α)
            # with a quadractic which is then minimised; this comes with a
            # guaranteed backtracking factor 0.5 * (1-c_1)^{-1} which is < 1
            # provided that c_1 < 1/2; the backtrack_condition at the beginning
            # of the function guarantees at least a backtracking factor ρ.
            α_tmp = - (dϕ_0 * α_2^2) / ( 2 * (ϕx_1 - ϕ_0 - dϕ_0*α_2) )
        else
            div = one(Tα) / (α_1^2 * α_2^2 * (α_2 - α_1))
            a = (α_1^2*(ϕx_1 - ϕ_0 - dϕ_0*α_2) - α_2^2*(ϕx_0 - ϕ_0 - dϕ_0*α_1))*div
            b = (-α_1^3*(ϕx_1 - ϕ_0 - dϕ_0*α_2) + α_2^3*(ϕx_0 - ϕ_0 - dϕ_0*α_1))*div

            if isapprox(a, zero(a), atol=eps(real(Tα)))
                α_tmp = dϕ_0 / (2*b)
            else
                # discriminant
                d = max(b^2 - 3*a*dϕ_0, Tα(0))
                # quadratic equation root
                @fastmath α_tmp = (-b + sqrt(d)) / (3*a)
            end
        end

        α_1 = α_2

        # α_tmp = NaNMath.min(α_tmp, α_2*ρ_hi) # avoid too small reductions
        # α_2 = NaNMath.max(α_tmp, α_2*ρ_lo) # avoid too big reductions
        α_tmp = min(α_tmp, α_2*ρ_hi) # avoid too small reductions
        α_2 = max(α_tmp, α_2*ρ_lo) # avoid too big reductions

        @fastmath @inbounds @simd for i ∈ 1:full_length(x_new)
            x_new[i] = x[i] + α_2 * s[i]
        end

        # Evaluate f(x) at proposed position
        ϕx_0, ϕx_1 = ϕx_1, value!(df, x_new)
        # @show ϕx_1, α_2
    end

    return α_2, ϕx_1
end


function part1(input)
    food_list = parse_input(input)
    all_ingredients_with_repetition = reduce(vcat, first.(food_list))
    potential_allergen_sources = find_potential_allergen_sources(food_list)
    potential_allergens = reduce(union, values(potential_allergen_sources))
    return length(filter(x -> x ∉ potential_allergens,
                         all_ingredients_with_repetition))
end

function part2(input)
    food_list = parse_input(input)
    potential_allergen_sources = find_potential_allergen_sources(food_list)
    allergen_sources = Dict{String, String}()
    while !isempty(potential_allergen_sources)
        for (allergen, ingredients) in potential_allergen_sources
            if length(ingredients) == 1
                ingredient = only(ingredients)
                allergen_sources[allergen] = ingredient
                delete!(potential_allergen_sources, allergen)
                filter!.(!=(ingredient), values(potential_allergen_sources))
                break
            end
        end
    end
    return join((allergen_sources[allergen]
                 for allergen in sort(collect(keys(allergen_sources)))), ",")
end

function parse_input(input)
    return parse_line.(readlines(input))
end

function parse_line(line)
    ingredients, allergens = match(r"([\w ]+) \(contains ([\w ,]+)\)", line).captures
    return String.(split(ingredients, " ")), String.(split(allergens, ", "))
end

function find_potential_allergen_sources(food_list)
    potential_allergen_sources = Dict{String, Vector{String}}()
    for (ingredients, allergens) in food_list
        for allergen in allergens
            if !haskey(potential_allergen_sources, allergen)
                potential_allergen_sources[allergen] = copy(ingredients)
            else
                intersect!(potential_allergen_sources[allergen], ingredients)
            end
        end
    end
    return potential_allergen_sources
end


#isdefined(:Tcl) || include("../src/Tcl.jl")

module TclBaseTests

using Tcl
using Test

# We need some irrational constants.
const π = MathConstants.π
const φ = MathConstants.φ

@testset "Basic Interface" begin

    @testset "Variables" begin
        i = Tcl.getinterp()
        for (name, value) in (("a", 42), ("1", 1), ("", "empty"),
                              ("π", π), ("w\0rld is beautiful!", true))
            # Check methods.
            Tcl.exec(TclStatus, "array", "unset", name)
            Tcl.setvar(name, value)
            if typeof(value) <: Union{String,Integer}
                @test Tcl.getvar(name) == value
            elseif typeof(value) <: AbstractFloat
                @test Tcl.getvar(name) ≈ value
            end
            @test Tcl.exists(name)
            Tcl.unsetvar(name)
            @test !Tcl.exists(name)

            # Check indexable interface.
            i[name] = value
            if typeof(value) <: Union{String,Integer}
                @test i[name] == value
            elseif typeof(value) <: AbstractFloat
                @test i[name] ≈ value
            end
            @test Tcl.exists(name)
            i[name] = nothing
            @test !Tcl.exists(name)
        end

        for (name1, name2, value) in (("a", "i", 42),
                                      ("1", "2", 12),
                                      ("", "", "really empty"),
                                      ("π", "φ", π),
                                      ("w\0rld is", "beautiful!", true))
            # Check methods.
            Tcl.unsetvar(name1, nocomplain=true)
            Tcl.setvar(name1, name2, value)
            if typeof(value) <: Union{String,Integer}
                @test Tcl.getvar(name1, name2) == value
            elseif typeof(value) <: AbstractFloat
                @test Tcl.getvar(name1, name2) ≈ value
            end
            @test Tcl.exists(name1, name2)
            Tcl.unsetvar(name1, name2)
            @test !Tcl.exists(name1, name2)

            # Check indexable interface.
            i[name1, name2] = value
            if typeof(value) <: Union{String,Integer}
                @test i[name1, name2] == value
            elseif typeof(value) <: AbstractFloat
                @test i[name1, name2] ≈ value
            end
            @test Tcl.exists(name1, name2)
            i[name1, name2] = nothing
            @test !Tcl.exists(name1, name2)
        end
    end

    @testset "Scalars" begin
        var = "x"
        @testset "Integers" begin
            for v in (1, -1, 0, 250, 1<<40)
                x = Tcl.exec(:set, var, v)
                @test x == Tcl.getvar(var)
                @test typeof(x) <: Integer
            end
        end
        @testset "Floats" begin
            for v in (1.0, 0.0, -0.5, π, φ, sqrt(2))
                x = Tcl.exec(:set, var, v)
                @test x == Tcl.getvar(var)
                @test typeof(x) <: Cdouble
            end
        end
        @testset "Strings" begin
            for v in ("", "hellow world!", "1", "true", " ", "\n", "\t", "\r",
                      "\a", "caleçon espiègle")
                x = Tcl.exec(:set, var, v)
                @test x == Tcl.getvar(var)
                @test typeof(x) <: String
            end
            for v in ("\u0", "\u2200", "\u2200 x \u2203 y")
                x = Tcl.exec(:set, var, TclObj(v))
                @test x == Tcl.getvar(var)
                @test typeof(x) <: String
            end
        end
        @testset "Booleans" begin
            for v in (true, false)
                x = Tcl.exec(:set, var, v)
                @test x == Tcl.getvar(var)
                @test typeof(x) <: Integer
            end
        end
    end

    @testset "Lists" begin
        wa = ["", "false", "hello world!", "caleçon espiègle"]
        wb = Tcl.exec(TclObj, "list", wa...)
        wc = Tcl.getvalue(wb)
        @test all(wc .== wa)
        @test eltype(wc) <: String

        xa = [false,true,0,1,2,3]
        xb = Tcl.exec(TclObj, "list", xa...)
        xc = Tcl.getvalue(xb)
        @test all(xc .== xa)
        @test eltype(xc) <: Integer

        ya = [-4,6,-7]
        yb = Tcl.exec(TclObj, "list", ya...)
        yc = Tcl.getvalue(yb)
        @test all(yc .== ya)
        @test eltype(yc) <: Integer

        za = [-1.0, 0.0, 1.0, -4.2, π, sqrt(2), φ]
        zb = Tcl.exec(TclObj, "list", za...)
        zc = Tcl.getvalue(zb)
        @test all(zc .≈ za)
        @test all(zc[1:3] .== za[1:3]) # no loss of precision?
        @test eltype(zc) <: AbstractFloat

        lst1 = Tcl.exec("list",xb,yb)
        @test eltype(lst1) <: Vector{<:Integer}
        @test all(lst1[1] == xa)
        @test all(lst1[2] == ya)

        lst2 = Tcl.exec("list",xb,"hello")
        @test typeof(lst2) == Vector{Any}
        @test all(lst2[1] .== xc)
        @test all(lst2[2] == "hello")

        lst3 = Tcl.exec("list",xb,yb,zb)
        @test typeof(lst3) == Vector{Any}
        @test all(lst3[1] .== xc)
        @test all(lst3[2] .== yc)
        @test all(lst3[3] .== zc)

        lst2b = Tcl.exec(TclObj,"list",xb,"hello")
        lst3b = Tcl.exec(TclObj,"list",xb,yb,zb)
        lst4 = Tcl.exec("list",lst2b,lst3b)
        @test typeof(lst4) == Vector{Any}
        @test length(lst4) == 2
        @test all(lst4[1][1] .== xc)
        @test all(lst4[1][2] == "hello")
        @test all(lst4[2][1] .== xc)
        @test all(lst4[2][2] .== yc)
        @test all(lst4[2][3] .== zc)

        lst5 = Tcl.list(π, 1, "hello", 2:6)
        @test length(lst5) == 4
        @test lst5[1] ≈ π
        @test lst5[2] == 1
        @test lst5[3] == "hello"
        @test all(lst5[4] .== 2:6)
        push!(lst5, sqrt(2))
        @test length(lst5) == 5
        @test lst5[end] ≈ sqrt(2)
        @test all(lst5[4:end][1] .== 2:6)
        @test lst5[0] == nothing
        @test lst5[end+1] == nothing
        A = lst5[[1 2; 3 4; 5 6]]
        @test size(A) == (3, 2)
        @test A[1,1] == lst5[1]
        @test A[1,2] == lst5[2]
        @test A[2,1] == lst5[3]
        @test A[2,2] == lst5[4]
        @test A[3,1] == lst5[5]
        @test A[3,2] == lst5[6]

        #yc = Tcl.exec("list",x,z)
        #r = Tcl.exec(TclObj,"list",4.6,pi)
        #q = Tcl.exec(TclObj,"list",4,pi)
        #Tcl.exec("list",4,pi)
        #Tcl.exec("list",x,z,r)
        #Tcl.exec("list",x,z,"hello",r)
        #i = Tcl.getinterp()
        #i[:v] = r
        #Tcl.eval(i, raw"foreach x $v { puts $x }")

    end
end

end


println("including tssm_schroedinger.jl for type ", T)
for (METHOD, SUF, COMPLEX_METHOD, DIM) in (
                 (:Schroedinger1D, :_schroedinger_1d, true, 1 ),
                 (:Schroedinger2D, :_schroedinger_2d, true, 2 ),
                 (:Schroedinger3D, :_schroedinger_3d, true, 3 ),
                 (:SchroedingerReal1D, :_schroedinger_real_1d, false, 1 ),
                 (:SchroedingerReal2D, :_schroedinger_real_2d, false, 2 ),
                 (:SchroedingerReal3D, :_schroedinger_real_3d, false, 3 ),
                 (:SchroedingerRotating2D, :_schroedinger_rotating_2d, true, 2 ),
                 (:SchroedingerRotating3D, :_schroedinger_rotating_3d, true, 3 ),
                 (:SchroedingerRotatingReal2D, :_schroedinger_rotating_real_2d, false, 2 ),
                 (:SchroedingerRotatingReal3D, :_schroedinger_rotating_real_3d, false, 3 ),
                 (:SchroedingerHermite1D, :_schroedinger_hermite_1d, true, 1 ),
                 (:SchroedingerHermite2D, :_schroedinger_hermite_2d, true, 2 ),
                 (:SchroedingerHermite3D, :_schroedinger_hermite_3d, true, 3 ),
                 (:SchroedingerHermiteReal1D, :_schroedinger_hermite_real_1d, false, 1 ),
                 (:SchroedingerHermiteReal2D, :_schroedinger_hermite_real_2d, false, 2 ),
                 (:SchroedingerHermiteReal3D, :_schroedinger_hermite_real_3d, false, 3 ),
                 (:SchroedingerGeneralizedLaguerre2D, :_schroedinger_gen_laguerre_2d, true, 2 ),
                 (:SchroedingerGeneralizedLaguerreHermite3D, :_schroedinger_gen_laguerre_hermite_3d, true, 3 ),
                 (:SchroedingerGeneralizedLaguerreReal2D, :_schroedinger_gen_laguerre_real_2d, false, 2 ),
                 (:SchroedingerGeneralizedLaguerreHermiteReal3D, :_schroedinger_gen_laguerre_hermite_real_3d, false, 3 ),
                )
    println("    ", METHOD)
    if T == :Float128
        SUF = Symbol(SUF, "_wf128")
    end
    WF = Symbol(:Wf,METHOD)
    @eval begin

        function get_hbar(m::($METHOD){$T})
           ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"get_hbar",SUF))), ($T),
                 (Ptr{Nothing}, ), m.m )
        end

        function get_cubic_coupling(m::($METHOD){$T})
           ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"get_cubic_coupling",SUF))), ($T),
                 (Ptr{Nothing}, ), m.m )
        end

        function set_cubic_coupling!(m::($METHOD){$T}, cc::Real)
           ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"set_cubic_coupling",SUF))), Nothing,
                 (Ptr{Nothing}, ($T)), m.m, cc )
        end

        function load_potential!(m::($METHOD){$T}, filename::AbstractString)
           ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"load_potential",SUF))), Nothing,
                 (Ptr{Nothing}, Cstring, Int32,), m.m, filename, length(filename))
        end

        function save_potential(m::($METHOD){$T}, filename::AbstractString)
           ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"save_potential",SUF))), Nothing,
                 (Ptr{Nothing}, Cstring, Int32,), m.m, filename, length(filename))
        end

        function kinetic_energy(psi::($WF){$T})
           ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"kinetic_energy_wf",SUF))), ($T),
                 (Ptr{Nothing}, ), psi.p )
        end

        function potential_energy(psi::($WF){$T})
           ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"potential_energy_wf",SUF))), ($T),
                 (Ptr{Nothing}, ), psi.p )
        end

        function interaction_energy(psi::($WF){$T})
           ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"interaction_energy_wf",SUF))), ($T),
                 (Ptr{Nothing}, ), psi.p )
        end

        function get_energy_expectation_deviation(psi::($WF){$T})
           ans = zeros(($T), 2)
           ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"get_energy_expectation_deviation_wf",SUF))), Nothing,
                (Ptr{Nothing}, Ptr{$T}), psi.p, ans )
           tuple(ans...)
        end

        function get_realspace_observables(psi::($WF){$T})
           ans = zeros(($T), 2+2*($DIM))
           ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"get_realspace_observables_wf",SUF))), Nothing,
                (Ptr{Nothing}, Ptr{$T}), psi.p, ans )
           tuple(ans...)
        end

    end #eval

    if COMPLEX_METHOD
        @eval begin
            function propagate_B!(psi::($WF){$T}, dt::Number)
                ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"propagate_B_wf",SUF))), Nothing,
                        (Ptr{Nothing}, Complex{$T},), psi.p, dt)
            end

            function propagate_B_derivative!(this::($WF){$T}, other::($WF){$T}, dt::Number)
               if this.m ≠ other.m
                   error("this and other must belong to the same method")
               end
               ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"propagate_B_derivative_wf",SUF))), Nothing,
                                (Ptr{Nothing}, Ptr{Nothing}, Complex{$T}),
                                 this.p, other.p, dt)
            end

            function imaginary_time_propagate_A!(psi::($WF){$T}, dt::Number)
               ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"imaginary_time_propagate_A_wf",SUF))), Nothing,
                                (Ptr{Nothing}, Complex{$T},), psi.p, dt)
            end

            function imaginary_time_propagate_B!(psi::($WF){$T}, dt::Number, method_for_B::Integer=0)
               ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"imaginary_time_propagate_B_wf",SUF))), Nothing,
                                (Ptr{Nothing}, Complex{$T}, Int32), psi.p, dt, method_for_B)
            end

            function add_apply_B!(this::($WF){$T}, other::($WF){$T},
                                 coefficient::Number=1.0)
               if this.m ≠ other.m
                   error("this and other must belong to the same method")
               end
               ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"add_apply_B_wf",SUF))), Nothing,
                                (Ptr{Nothing}, Ptr{Nothing}, Complex{$T}),
                                 this.p, other.p, coefficient)
            end

            function selfconsistent_nonlinear_step!(psi::($WF){$T}, dt::Number,
                           dt1::Number, eps::Number=100.0_prec*eps(($T)), max_iters::Integer=30)
               ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"selfconsistent_nonlinear_step_wf",SUF))), Nothing,
                          (Ptr{Nothing}, Complex{$T}, Complex{$T}, ($T), Int32), psi.p, dt, dt1, eps, max_itesr)
            end

            function kinetic_matrix_element(psi1::($WF){$T}, psi2::($WF){$T})
                if psi1.m ≠ psi2.m
                    error("psi1 and psi2 must belong to the same method")
                end
                ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"kinetic_matrix_element_wf",SUF))), (Complex{$T}),
                    (Ptr{Nothing}, Ptr{Nothing}), psi1.p, psi2.p )
            end

            function potential_matrix_element(psi1::($WF){$T}, psi2::($WF){$T})
                if psi1.m ≠ psi2.m
                    error("psi1 and psi2 must belong to the same method")
                end
                ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"potential_matrix_element_wf",SUF))), (Complex{$T}),
                    (Ptr{Nothing}, Ptr{Nothing}), psi1.p, psi2.p )
            end

         end #eval
    else
        @eval begin
            function imaginary_time_propagate_A!(psi::($WF){$T}, dt::Real)
               ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"imaginary_time_propagate_A_wf",SUF))), Nothing,
                                (Ptr{Nothing}, ($T),), psi.p, dt)
            end

            function imaginary_time_propagate_B!(psi::($WF){$T}, dt::Real, method_for_B::Integer=0)
               ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"imaginary_time_propagate_B_wf",SUF))), Nothing,
                                (Ptr{Nothing}, ($T), Int32), psi.p, dt, method_for_B)
            end

            function add_apply_B!(this::($WF){$T}, other::($WF){$T},
                                 coefficient::Number=1.0)
               if this.m ≠ other.m
                   error("this and other must belong to the same method")
               end
               ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"add_apply_B_wf",SUF))), Nothing,
                                (Ptr{Nothing}, Ptr{Nothing}, ($T)),
                                 this.p, other.p, coefficient)
            end

            function selfconsistent_nonlinear_step!(psi::($WF){$T}, dt::Number,
                           dt1::Number, eps::Number=100.0_prec*eps(($T)), max_iters::Integer=30)
               ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"selfconsistent_nonlinear_step_wf",SUF))), Nothing,
                          (Ptr{Nothing}, ($T), ($T), ($T), Int32), psi.p, dt, dt1, eps, max_itesr)
            end

            function kinetic_matrix_element(psi1::($WF){$T}, psi2::($WF){$T})
                if psi1.m ≠ psi2.m
                    error("psi1 and psi2 must belong to the same method")
                end
                ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"kinetic_matrix_element_wf",SUF))), ($T),
                    (Ptr{Nothing}, Ptr{Nothing}), psi1.p, psi2.p )
            end

            function potential_matrix_element(psi1::($WF){$T}, psi2::($WF){$T})
                if psi1.m ≠ psi2.m
                    error("psi1 and psi2 must belong to the same method")
                end
                ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"potential_matrix_element_wf",SUF))), ($T),
                    (Ptr{Nothing}, Ptr{Nothing}), psi1.p, psi2.p )
            end

        end #eval
    end #if

    if DIM==1
        @eval begin
            function set_potential!(m::($METHOD){$T}, f::Function)
               f_c = cfunction_check_return_type(f, ($T), (($T),))
               ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"set_potential",SUF))), Nothing,
                     (Ptr{Nothing}, Ptr{Nothing}), m.m, f_c )
            end

            function get_potential(m::($METHOD){$T}, unsafe_access::Bool=false)
               dims = zeros(Int32, 1)
               Vp = ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"get_potential",SUF))), Ptr{$T},
                     (Ptr{Nothing}, Ptr{Int32}), m.m, dims )
               if Vp==C_NULL
                   return zeros(($T), dims[1])
               end
               V = unsafe_wrap(Array, Vp, dims[1], own=false)
               if unsafe_access
                  return V
               else
                  return copy(V)
               end
            end

            function observable(psi::($WF){$T}, f::Function)
               f_c = cfunction_check_return_type(f, ($T), (($T), ))
               ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"observable_wf",SUF))), ($T),
                     (Ptr{Nothing}, Ptr{Nothing} ), psi.p, f_c )
            end

        end #eval
        if COMPLEX_METHOD
        @eval begin
            function set_potential_t!(m::($METHOD){$T}, f::Function)
               f_c = cfunction_check_return_type(f, ($T), (($T),($T)))
               ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"set_potential_t",SUF))), Nothing,
                     (Ptr{Nothing}, Ptr{Nothing}, Bool), m.m, f_c, false )
            end

            function set_potential_t_derivative!(m::($METHOD){$T}, f::Function)
               f_c = cfunction_check_return_type(f, ($T), (($T),($T)))
               ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"set_potential_t",SUF))), Nothing,
                     (Ptr{Nothing}, Ptr{Nothing}, Bool), m.m, f_c, true )
            end

            function get_potential_t(m::($METHOD){$T}, t::Real)
               dims = zeros(Int32, 1)
               Vp = ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"get_potential_t",SUF))), Ptr{$T},
                     (Ptr{Nothing}, ($T), Bool, Ptr{Int32}), m.m, t, false, dims )
               if Vp==C_NULL
                   return zeros(($T), dims[1])
               end
               V = unsafe_wrap(Array, Vp, dims[1], own=false)
               return copy(V)
            end

            function get_potential_t_derivative(m::($METHOD){$T}, t::Real)
               dims = zeros(Int32, 1)
               Vp = ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"get_potential_t",SUF))), Ptr{$T},
                     (Ptr{Nothing}, ($T), Bool, Ptr{Int32}), m.m, t, true, dims )
               if Vp==C_NULL
                   return zeros(($T), dims[1])
               end
               V = unsafe_wrap(Array, Vp, dims[1], own=false)
               return copy(V)
            end

        end #eval
        end #if
    elseif DIM==2
        @eval begin
            function set_potential!(m::($METHOD){$T}, f::Function)
               f_c = cfunction_check_return_type(f, ($T), (($T), ($T)))
               ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"set_potential",SUF))), Nothing,
                     (Ptr{Nothing}, Ptr{Nothing}), m.m, f_c )
            end

            function get_potential(m::($METHOD){$T}, unsafe_access::Bool=false)
               dims = zeros(Int32, 2)
               Vp = ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"get_potential",SUF))), Ptr{$T},
                     (Ptr{Nothing}, Ptr{Int32}), m.m, dims )
               if Vp==C_NULL
                   return zeros(($T), dims[1], dims[2])
               end
               V = unsafe_wrap(Array, Vp, dims[1], dims[2], own=false)
               if unsafe_access
                  return V
               else
                  return copy(V)
               end
            end

            function observable(psi::($WF){$T}, f::Function)
               f_c = cfunction_check_return_type(f, ($T), (($T), ($T)))
               ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"observable_wf",SUF))), ($T),
                     (Ptr{Nothing}, Ptr{Nothing} ), psi.p, f_c)
            end

        end #eval
        if COMPLEX_METHOD
        @eval begin
            function set_potential_t!(m::($METHOD){$T}, f::Function)
               f_c = cfunction_check_return_type(f, ($T), (($T),($T),($T)))
               ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"set_potential_t",SUF))), Nothing,
                     (Ptr{Nothing}, Ptr{Nothing}, Bool), m.m, f_c, false )
            end

            function set_potential_t_derivative!(m::($METHOD){$T}, f::Function)
               f_c = cfunction_check_return_type(f, ($T), (($T),($T),($T)))
               ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"set_potential_t",SUF))), Nothing,
                     (Ptr{Nothing}, Ptr{Nothing}, Bool), m.m, f_c, true )
            end

            function get_potential_t(m::($METHOD){$T}, t::Real)
               dims = zeros(Int32, 2)
               Vp = ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"get_potential_t",SUF))), Ptr{$T},
                     (Ptr{Nothing}, ($T), Bool, Ptr{Int32}), m.m, t, false, dims )
               if Vp==C_NULL
                   return zeros(($T), dims[1], dims[2])
               end
               V = unsafe_wrap(Array, Vp, dims[2], own=false)
               return copy(V)
            end

            function get_potential_t_derivative(m::($METHOD){$T}, t::Real)
               dims = zeros(Int32, 2)
               Vp = ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"get_potential_t",SUF))), Ptr{$T},
                     (Ptr{Nothing}, ($T), Bool, Ptr{Int32}), m.m, t, true, dims )
               if Vp==C_NULL
                   return zeros(($T), dims[1], dims[2])
               end
               V = unsafe_wrap(Array, Vp, dims[2], own=false)
               return copy(V)
            end

        end #eval
        end #if
    elseif DIM==3
        @eval begin
            function set_potential!(m::($METHOD){$T}, f::Function)
               f_c = cfunction_check_return_type(f, ($T), (($T), ($T), ($T)))
               ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"set_potential",SUF))), Nothing,
                     (Ptr{Nothing}, Ptr{Nothing}), m.m, f_c )
            end

            function get_potential(m::($METHOD){$T}, unsafe_access::Bool=false)
               dims = zeros(Int32, 3)
               Vp = ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"get_potential",SUF))), Ptr{$T},
                     (Ptr{Nothing}, Ptr{Int32}), m.m, dims )
               if Vp==C_NULL
                   return zeros(($T), dims[1], dims[2], dims[3])
               end
               V = unsafe_wrap(Array, Vp, dims[1], dims[2], dims[3], own=false)
               if unsafe_access
                  return V
               else
                  return copy(V)
               end
            end

            function observable(psi::($WF){$T}, f::Function)
               f_c = cfunction_check_return_type(f, ($T), (($T), ($T), ($T)))
               ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"observable_wf",SUF))), ($T),
                     (Ptr{Nothing}, Ptr{Nothing} ), psi.p, f_c )
            end

        end #eval
        if COMPLEX_METHOD
        @eval begin
            function set_potential_t!(m::($METHOD){$T}, f::Function)
               f_c = cfunction_check_return_type(f, ($T), (($T),($T),($T)($T)))
               ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"set_potential_t",SUF))), Nothing,
                     (Ptr{Nothing}, Ptr{Nothing}, Bool), m.m, f_c, false )
            end

            function set_potential_t_derivative!(m::($METHOD){$T}, f::Function)
               f_c = cfunction_check_return_type(f, ($T), (($T),($T),($T)($T)))
               ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"set_potential_t",SUF))), Nothing,
                     (Ptr{Nothing}, Ptr{Nothing}, Bool), m.m, f_c, true )
            end

            function get_potential_t(m::($METHOD){$T}, t::Real)
               dims = zeros(Int32, 3)
               Vp = ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"get_potential_t",SUF))), Ptr{$T},
                     (Ptr{Nothing}, ($T), Bool, Ptr{Int32}), m.m, t, false, dims )
               if Vp==C_NULL
                   return zeros(($T), dims[1], dims[2], dims[3])
               end
               V = unsafe_wrap(Array, Vp, dims[3], own=false)
               return copy(V)
            end

            function get_potential_t_derivative(m::($METHOD){$T}, t::Real)
               dims = zeros(Int32, 3)
               Vp = ccall( Libdl.dlsym(($TSSM_HANDLE), $(string(PRE,"get_potential_t",SUF))), Ptr{$T},
                     (Ptr{Nothing}, ($T), Bool, Ptr{Int32}), m.m, t, true, dims )
               if Vp==C_NULL
                   return zeros(($T), dims[1], dims[2], dims[3])
               end
               V = unsafe_wrap(Array, Vp, dims[3], own=false)
               return copy(V)
            end

        end #eval
        end #if
    end #if

end # for


__precompile__()

module Andes

using Conda
using PyCall

import SparseArrays

const py = PyNULL()

include("kvxopt.jl")

function __init__()
    copy!(py, pyimport_conda("andes", "andes", "conda-forge"))

    pytype_mapping(pyimport("kvxopt").spmatrix, SparseArrays.SparseMatrixCSC)
end

# --- export ---
export convert;

end # module


# start the following on the command line:
# julia -p n  # starts n processes on the local machine
# julia -p 8  # starts REPL with 8 workers
workers()
# 8-element Array{Int64,1}:
#   2
#   3
#   4
#   5
#   ⋮
#   8
#   9

# iterate over workers:
for pid in workers()
	# do something with pid
end
myid() # 1
addprocs(5)
# 5-element Array{Any,1}:
#  10
#  11
#  12
#  13
#  14
nprocs() # 14
rmprocs(3) # worker with id 3 is removed

# cluster of computers:
# julia --machinefile machines driver.jl
# Run processes specified in driver.jl on hosts listed in machines

# primitive operations with processes:
r1 = remotecall(2, x -> x^2, 1000) #> RemoteRef(2,1,20)
fetch(r1) #> 1000000

remotecall_fetch(5, sin, 2pi) # -2.4492935982947064e-16

r2 = @spawnat 4 sqrt(2) # lets worker 4 calculate sqrt(2)
fetch(r2)  #> 1.4142135623730951
r = [@spawnat w sqrt(5) for w in workers()]
# or in a freshly started REPL:
# r = [@spawnat i sqrt(5) for i=1:length(workers())]
# 6-element Array{Any,1}:
#  RemoteRef(1,1,8)
#  RemoteRef(2,1,9)
#  RemoteRef(3,1,10)
#  RemoteRef(4,1,11)
#  RemoteRef(5,1,12)
#  RemoteRef(6,1,13)
r3 = @spawn sqrt(5) #> RemoteRef(5,1,26)
fetch(r3) #> 2.23606797749979

# using @everywhere to make functions available to all workers:
@everywhere w = 8
@everywhere println(myid()) # 1
        # From worker 2:  2
        # From worker 3:  3
        # From worker 4:  4
        # From worker 7:  7
        # From worker 5:  5
        # From worker 6:  6
        # From worker 8:  8
        # From worker 9:  9

@everywhere include("defs.jl")
@everywhere function fib(n)
	if (n < 2) then
	return n
	else return fib(n-1) + fib(n-2)
	end
end
require("functions")

# broadcast data to all workers:
d = "Julia"
for pid in workers()
   remotecall(pid, x -> (global d; d = x; nothing), d)
end
#= Output:
1   From worker 2:  2

    From worker 4:  4
    From worker 5:  5
    From worker 6:  6
=#


using ArgParse
include("../specfem_gll.jl/tasks/numerical_operation_on_bin_file.jl")
include("../specfem_gll.jl/src/utils/readfiles.jl")

function parse_command_line()
    s = ArgParseSettings()

    @add_arg_table! s begin
        "--target_basedir"
            help = "the target gll directory"
        "--reference_basedir"
            help = "the reference gll directory"
        "--mesh_basedir"
            help = "the mesh directory"
        "--output_basedir"
            help = "the output gll directory for perturbation"
        "--tags"
            help = "the tags, eg: alpha,beta"
        "--nproc"
            help = "number of processors the gll directory correspons to"
    end
    return parse_args(s)
    end

function main()
    # parse args
    parsed_args = parse_command_line()
    target_basedir = parsed_args["target_basedir"]
    mesh_basedir = parsed_args["mesh_basedir"]
    reference_basedir = parsed_args["reference_basedir"]
    output_basedir = parsed_args["output_basedir"]
    tags = parsed_args["tags"]
    nproc = parse(Int64, parsed_args["nproc"])
    # get nspec
    mesh_info = sem_mesh_read(mesh_basedir, 0)
    nspec = mesh_info.nspec
    # run generate_perturbation
    generate_difference(target_basedir, reference_basedir, output_basedir, tags, nproc, nspec)
end

main()


################################################################################
#
#	ABSTRACT TYPE
#
#   BondHoppingHamiltonianSimple <: AbstractBondHamiltonian{L,1}
#   --> L is the label type of bonds
#   --> N is the dimension the bond term matrix (NxN matrix)
#
#   FILE CONTAINS
#       - abstract type definition
#       - interface definition
#       - generator function
#
################################################################################

################################################################################
#
#   ABSTRACT TYPE DEFINITION
#
################################################################################
mutable struct BondHoppingHamiltonianSimple{L} <: AbstractBondHamiltonian{L,1}

    # coupling
    coupling :: Float64

end

# export the type
export BondHoppingHamiltonianSimple

################################################################################
#
#	INTERFACING / ACCESSING BOND HAMILTONIANS
#	(functions have to be overwritten by concrete types)
#
################################################################################

# get bond term of Hamiltonian
function bondterm(
            h :: BondHoppingHamiltonianSimple{L},
            b :: AbstractBond{L,NB}
        ) :: Float64 where {N,L,NB,NS}

    # every bond has the same coupling
    return h.coupling
end

# export the function
export bondterm

function getHoppingHamiltonianSimple(
            unitcell :: U
        ) :: BondHoppingHamiltonianSimple{L} where {L,NB,S,B<:AbstractBond{L,NB},U<:AbstractUnitcell{S,B}}

    # just create a new hamiltonian
    return BondHoppingHamiltonianSimple{L}(1.0)
end

export getHoppingHamiltonianSimple

function getBondHamiltonianType(
        ::Val{:BondHoppingHamiltonianSimple}
    )

    # return the type
    return BondHoppingHamiltonianSimple
end

function saveBondHamiltonian(
        hb :: HB,
        fn :: AbstractString,
        group :: AbstractString = "bond_hamiltonian"
        ;
        append :: Bool = false
    ) where {L,HB<:BondHoppingHamiltonianSimple{L}}

    # determine the mode based on if one wants to append stuff
    if append
        mode = "r+"
    else
        mode = "w"
    end

    # open the file in mode
    h5open(fn, mode) do file
        # create the group in which the bonds are saved
        group_hb = g_create(file, group)
        # save the type identifier
        attrs(group_hb)["type"] = "BondHoppingHamiltonianSimple"
        # save the parameters
        attrs(group_hb)["N"] = 1
        attrs(group_hb)["L"] = string(L)
        # save the Float64 coupling
        group_hb["coupling"] = hb.coupling
    end

    # return nothing
    return nothing
end

function loadBondHamiltonian(
        ::Type{HB},
        fn :: AbstractString,
        group :: AbstractString = "bond_hamiltonian"
    ) where {LI,HB<:Union{BondHoppingHamiltonianSimple{LI},BondHoppingHamiltonianSimple}}

    # read attribute data
    attr_data = h5readattr(fn, group)
    # determine D based on this
    L = Meta.eval(Meta.parse(attr_data["L"]))
    N = attr_data["N"]

    # load all remaining data
    coupling = h5read(fn, group*"/coupling")

    # return the new bond hamiltonian
    return BondHoppingHamiltonianSimple{L}(coupling)
end


function GroundFieldOfViewDetector(arg0::Frame, arg1::FieldOfView)
    return GroundFieldOfViewDetector((Frame, FieldOfView), arg0, arg1)
end

function g(obj::GroundFieldOfViewDetector, arg0::SpacecraftState)
    return jcall(obj, "g", jdouble, (SpacecraftState,), arg0)
end

function get_fov(obj::GroundFieldOfViewDetector)
    return jcall(obj, "getFOV", FieldOfView, ())
end

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


# This file defines a hierarchy of typed expressions

##########################################################################
#
#   Typed expressions
#
##########################################################################

# modes: the role/functionality of an expression

abstract TMode
type ScalarMode <: TMode end
type EWiseMode{D} <: TMode end
type ReducMode <: TMode end
type ColwiseReducMode <: TMode end
type RowwiseReducMode <: TMode end

# types

abstract TExpr

# expressions that can be an argument for element-wise computation
abstract TEWise <: TExpr

abstract TGeneralVar <: TEWise

# expressions that is sured to be a scalar
abstract TScalar <: TEWise

type TEmpty <: TExpr end

# simple expressions

type TNum{T<:Number} <: TScalar
    val::T
end
ju_expr(tx::TNum) = tx.val

==(a::TNum, b::TNum) = (a.val == b.val)
!=(a::TNum, b::TNum) = !(a == b)

type TScalarVar <: TScalar
    name::Symbol
end
ju_expr(tx::TScalarVar) = tx.name

scalar(x::Number) = x

==(a::TScalarVar, b::TScalarVar) = (a.name == b.name)
!=(a::TScalarVar, b::TScalarVar) = !(a == b)

type TVar <: TGeneralVar
    name::Symbol
end
ju_expr(tx::TVar) = tx.name

==(a::TVar, b::TVar) = (a.name == b.name)
!=(a::TVar, b::TVar) = !(a == b)

# qualified variable, e.g. abc.x
type TQVar <: TGeneralVar
    form::Expr
end
ju_expr(tx::TQVar) = tx.form

==(a::TQVar, b::TQVar) = (a.form == b.form)
!=(a::TQVar, b::TQVar) = !(a == b)

type TGeneralScalar <: TScalar
    form::Expr
end
ju_expr(tx::TGeneralScalar) = tx.form

as_scalar(a::TScalar) = a
as_scalar(a::TVar) = TScalarVar(a.name)
as_scalar(a::TQVar) = TGeneralScalar(a.form)
as_scalar(a::TGeneralVar) = TGeneralScalar(ju_expr(a))
as_scalar(a::TExpr) = throw(DeError("Expression of type $(typeof(a)) can not be tagged as scalar."))

# references

abstract TRef <: TEWise

@compat typealias TIndex Union{Int,Symbol}
abstract TRange

type TColon <: TRange end
@compat type TInterval <: TRange
    first::TIndex
    last::Union{TIndex,Void}
end

==(a::TInterval, b::TInterval) = (a.first == b.first) && (a.last == b.last)
!=(a::TInterval, b::TInterval) = !(a == b)

# integer index
type TScalarRef1 <: TGeneralVar # or TGeneralScalar??
    host::TGeneralVar
    i::Any
end
ju_expr(tx::TScalarRef1) = :( $(ju_expr(tx.host))[$(tx.i)] )

==(a::TScalarRef1, b::TScalarRef1) = (a.host == b.host) && (a.i == b.i)
!=(a::TScalarRef1, b::TScalarRef1) = !(a == b)

type TGeneralRef1 <: TRef
    host::TGeneralVar
    i::Any
end
ju_expr(tx::TGeneralRef1) = :( $(ju_expr(tx.host))[$(tx.i)] )
as_scalar(a::TGeneralRef1) = TGeneralScalar(ju_expr(a))

==(a::TGeneralRef1, b::TGeneralRef1) = (a.host == b.host) && (a.i == b.i)
!=(a::TGeneralRef1, b::TGeneralRef1) = !(a == b)

type TGeneralRef2 <: TGeneralVar
    host::TGeneralVar
    i::Any
    j::Any
end
ju_expr(tx::TGeneralRef2) = :( $(ju_expr(tx.host))[$(tx.i), $(tx.j)] )
as_scalar(a::TGeneralRef2) = TGeneralScalar(ju_expr(a))

==(a::TGeneralRef2, b::TGeneralRef2) = (a.host == b.host) && (a.i == b.i) && (a.j == b.j)
!=(a::TGeneralRef2, b::TGeneralRef2) = !(a == b)

type TRef1D <: TRef
    host::TGeneralVar
    rgn::TRange
end

==(a::TRef1D, b::TRef1D) = (a.host == b.host) && (a.rgn == b.rgn)
!=(a::TRef1D, b::TRef1D) = !(a == b)

type TRefCol <: TRef
    host::TGeneralVar
    rrgn::TRange
    icol::TIndex
end

==(a::TRefCol, b::TRefCol) = (a.host == b.host) && (a.rrgn == b.rrgn) && (a.icol == b.icol)
!=(a::TRefCol, b::TRefCol) = !(a == b)

type TRefRow <: TRef
    host::TGeneralVar
    irow::TIndex
    crgn::TRange
end

==(a::TRefRow, b::TRefRow) = (a.host == b.host) && (a.irow == b.irow) && (a.crgn == b.crgn)
!=(a::TRefRow, b::TRefRow) = !(a == b)

type TRef2D <: TRef
    host::TGeneralVar
    rrgn::TRange
    crgn::TRange
end

==(a::TRef2D, b::TRef2D) = (a.host == b.host) && (a.rrgn == b.rrgn) && (a.crgn == b.crgn)
!=(a::TRef2D, b::TRef2D) = !(a == b)

# function calls

@compat type TMap <: TEWise
    fun::Symbol
    args::Tuple{Vararg{TEWise}}
    mode::TMode
    deps::Union{Array{TExpr},Void}
end

function ju_expr(tx::TMap)
    ju_args = [ju_expr(a) for a in tx.args]
    Expr(:call, tx.fun, ju_args...)
end
as_scalar(tx::TMap) = TGeneralScalar(ju_expr(tx))

@compat type TReduc <: TExpr
    fun::Symbol
    args::Tuple{Vararg{TEWise}}
    arg_mode::TMode
    deps::Union{Array{TExpr},Void}
end

@compat type TColwiseReduc <: TExpr
    fun::Symbol
    args::Tuple{Vararg{TEWise}}
    deps::Union{Array{TExpr},Void}
end

@compat type TRowwiseReduc <: TExpr
    fun::Symbol
    args::Tuple{Vararg{TEWise}}
    deps::Union{Array{TExpr},Void}
end

@compat typealias TFunCall Union{TMap,TReduc,TColwiseReduc,TRowwiseReduc}

function ==(a::TFunCall, b::TFunCall)
    na = length(a.args)
    if a.fun == b.fun && typeof(a) == typeof(b) && na == length(b.args)
        for i = 1 : na
            if !(a.args[i] == b.args[i])
                return false
            end
        end
        return true
    end
    return false
end

!=(a::TFunCall, b::TFunCall) = !(a == b)

# others

@compat typealias TRValue Union{TEWise,TFunCall}
@compat typealias TLValue Union{TGeneralVar,TRef}

type TAssign{Lhs<:TLValue, Rhs<:TRValue} <: TExpr
    lhs::Lhs
    rhs::Rhs
    mode::TMode
end

==(a::TAssign, b::TAssign) = (a.lhs == b.lhs) && (a.rhs == b.rhs)
!=(a::TAssign, b::TAssign) = !(a == b)

is_trivial_assignment(ex::TAssign) = isa(ex.lhs, TGeneralVar) &&
    (isa(ex.rhs, TGeneralVar) || isa(ex.rhs, TNum))

type TBlock <: TExpr
    stmts::Array{TAssign}
    TBlock() = new(TAssign[])
end

==(a::TBlock, b::TBlock) = length(a.stmts) == length(b.stmts) && all(a.stmts .== b.stmts)
!=(a::TBlock, b::TBlock) = !(a == b)

##########################################################################
#
#   Determining the expression mode
#
#   i.e. what kind of things that the whole expression wants to do
#   (ewise, reduc, etc)
#
##########################################################################

tmode_num{D}(::EWiseMode{D}) = D

tmode(ex::TScalar) = ScalarMode()
tmode(ex::TScalarRef1) = EWiseMode{0}()
tmode(ex::TGeneralVar) = EWiseMode{0}()
tmode(ex::TGeneralRef1) = EWiseMode{0}()

tmode(ex::TRef1D) = EWiseMode{1}()
tmode(ex::TRef2D) = EWiseMode{2}()
tmode(ex::TRefCol) = EWiseMode{1}()
tmode(ex::TRefRow) = EWiseMode{1}()

tmode(ex::TMap) = ex.mode
tmode(ex::TReduc) = isa(ex.arg_mode, ScalarMode) ? ScalarMode() : ReducMode()
tmode(ex::TColwiseReduc) = ColwiseReducMode()
tmode(ex::TRowwiseReduc) = RowwiseReducMode()

tmode(ex::TAssign) = ex.mode

promote_ewise_tmode(m::TMode) = m
promote_ewise_tmode(m1::ScalarMode, m2::ScalarMode) = ScalarMode()
promote_ewise_tmode(m1::ScalarMode, m2::EWiseMode) = EWiseMode{tmode_num(m2)}()
promote_ewise_tmode(m1::EWiseMode, m2::ScalarMode) = EWiseMode{tmode_num(m1)}()

function promote_ewise_tmode(m1::EWiseMode, m2::EWiseMode)
    d1 = tmode_num(m1)
    d2 = tmode_num(m2)

    d1 == d2 ? EWiseMode{d1}() :
    d1 == 0 ? EWiseMode{d2}() :
    d2 == 0 ? EWiseMode{d1}() :
    throw(DeError("Incompatible ewise mode."))
end

promote_ewise_tmode(m1::TMode, m2::TMode, m3::TMode) = promote_ewise_tmode(promote_ewise_tmode(m1, m2), m3)

##########################################################################
#
#   functions to construct simple components of a typed expression
#
##########################################################################

type DeError <: Exception
    msg::ASCIIString
end

# number literals

tnum(x::Number) = TNum{typeof(x)}(x)

# variable

tvar(s::Symbol) = TVar(s)
tscalarvar(s::Symbol) = TScalarVar(s)

# qualified variable

function is_valid_tqvar(ex::Expr)
    @assert ex.head == :(.) && length(ex.args) == 2

    a1 = ex.args[1]
    a2 = ex.args[2]

    (isa(a1, Symbol) || is_valid_tqvar(a1)) &&
    (   isa(a2, QuoteNode) ||
        (a2.head == (:quote) && isa(a2.args[1], Symbol))
    )
end

function tqvar(ex::Expr)
    if !is_valid_tqvar(ex)
        throw(DeError("$ex is not a valid form for variable."))
    end
    TQVar(ex)
end

# reference expressions

function check_ref_validity(ex::Expr, c::Bool)
    if !c
        throw(DeError("The ref-expression $ex is not supported yet."))
    end
end

tref_arg(i::Any) = i
tref_arg(i::Int) = i
tref_arg(i::Symbol) = i == :(:) ? TColon() : i

function tref_arg(ex::Expr)
    if ex.head == :(:)
        check_ref_validity(ex, length(ex.args) == 2)

        a1 = ex.args[1]
        a2 = ex.args[2]

        if isa(a1, Int) || (isa(a1, Symbol) && a1 != :(:))
            first = a1
        else
            check_ref_validity(ex, false)
        end

        if isa(a2, Int)
            last = a2
        elseif isa(a2, Symbol)
            last = (a2 == :(:) || a2 == symbol("end")) ? nothing : a2
        else
            check_ref_validity(ex, false)
        end

        TInterval(first, last)
    else
        ex
    end
end

tref(x::TGeneralVar, i::Any) = TGeneralRef1(x, i)
tref(x::TGeneralVar, i::Int) = TScalarRef1(x, i)
tref(x::TGeneralVar, r::TRange) = TRef1D(x, r)

tref(x::TGeneralVar, i::Any, j::Any) = TGeneralRef2(x, i, j)
tref(x::TGeneralVar, i::TIndex, c::TRange) = TRefRow(x, i, c)
tref(x::TGeneralVar, r::TRange, j::TIndex) = TRefCol(x, r, j)
tref(x::TGeneralVar, r::TRange, c::TRange) = TRef2D(x, r, c)

function tref(ex::Expr)
    @assert ex.head == :(ref)

    na = length(ex.args)
    check_ref_validity(ex, na == 2 || na == 3)

    h = ex.args[1]
    if isa(h, Symbol)
        h = tvar(h)
    elseif isa(h, Expr) && h.head == :(.)
        h = tqvar(h)
    else
        check_ref_validity(ex, false)
    end

    if na == 2
        a1 = ex.args[2]
        tref(h, tref_arg(a1))

    else
        a1 = ex.args[2]
        a2 = ex.args[3]
        tref(h, tref_arg(a1), tref_arg(a2))
    end
end

##########################################################################
#
#   function call recognition
#
##########################################################################

is_ewise_call(f::Symbol, N::Int) = isa(get_op_kind(TCallSig{f, N}()), EWiseOp)
is_reduc_call(f::Symbol, N::Int) = isa(get_op_kind(TCallSig{f, N}()), ReducOp)

function check_funcall_args(args::TExpr...)
    na = length(args)
    deps = nothing

    pargs = Array(TExpr, na)

    for i = 1 : na
        a = args[i]
        if isa(a, TEWise)
            pargs[i] = a
        elseif isa(a, TReduc) || isa(a, TColwiseReduc) || isa(a, TRowwiseReduc)
            dep_sym = gensym("dep")
            pargs[i] = isa(a, TReduc) ? TScalarVar(dep_sym) : TVar(dep_sym)
            if deps == nothing
                deps = TExpr[]
            end
            push!(deps, tassign(TVar(dep_sym), a))
        else
            throw(DeError("Arguments in unsupported form."))
        end
    end
    (tuple(pargs...), deps)
end

# due to intricate semantics of partial reduction in Julia syntax
# we here hand-coded the specific form to be recognized as partial reduction

function recognize_partial_reduction(f::Symbol, a::TExpr...)
    if f == (:sum) || f == (:mean) || f == (:maximum) || f == (:minimum)
        if length(a) == 2 && isa(a[2], TNum{Int})
            fargs, deps = check_funcall_args(a[1])
            dim = a[2].val
            if dim == 1
                TColwiseReduc(f, fargs, deps)
            elseif dim == 2
                TRowwiseReduc(f, fargs, deps)
            else
                throw(DeError("Devectorize supports either colwise or rowwise reduction."))
            end
        end
    end
end

function tcall(f::Symbol, args)
    n = length(args)

    if f == (:scalar) && n == 1
        as_scalar(args[1])

    elseif is_ewise_call(f, n)
        fargs, deps = check_funcall_args(args...)
        mode = promote_ewise_tmode([tmode(a) for a in fargs]...)
        TMap(f, fargs, mode, deps)

    elseif is_reduc_call(f, n)
        fargs, deps = check_funcall_args(args...)
        arg_mode = promote_ewise_tmode([tmode(a) for a in fargs]...)
        TReduc(f, fargs, arg_mode, deps)

    elseif f == :(*)
        throw(DeError("Devectorize does not support *, please use .* to express element-wise multiplication."))

    elseif f == :(/)
        throw(DeError("Devectorize does not support /, please use ./ to express element-wise division."))

    elseif f == :(^)
        throw(DeError("Devectorize does not support ^, please use .^ to express element-wise power."))

    else
        ex = recognize_partial_reduction(f, args...)
        if ex == nothing
            throw(DeError("Unrecognized function $f with $n arguments (in Devectorize)"))
        end
        return ex
    end
end

function tcall(ex::Expr)
    @assert ex.head == :(call)
    fsym = ex.args[1]
    if !isa(fsym, Symbol)
        throw(DeError("call-expressions with non-symbol function name: $fsym"))
    end
    tcall(fsym, map(texpr, ex.args[2:end]))
end

function tcomparison(ex::Expr)
    @assert ex.head == :(comparison)
    opsym = ex.args[2]
    tcall(opsym, map(texpr, [ex.args[1], ex.args[3]]))
end

##########################################################################
#
#   assignment construction
#
##########################################################################

function decide_assign_tmode(lhs::TExpr, rhs::TExpr)

    # TScalarSym can only be created internally, which would never
    # be placed on the left hand side
    @assert !isa(lhs, TScalarVar)

    if isa(lhs, TGeneralVar)
        mode = tmode(rhs)

    elseif isa(lhs, TRef)
        @assert isa(lhs, TEWise)
        rmode = tmode(rhs)
        if isa(rmode, EWiseMode) || isa(rmode, ScalarMode)
            mode = promote_ewise_tmode(tmode(lhs), rmode)
        elseif isa(rmode, ColwiseReducMode) || isa(rmode, RowwiseReducMode)
            mode = rmode
        else
            throw(DeError("Incompatible lhs and rhs."))
        end

    else
        throw(DeError("Incompatible modes between lhs and rhs."))
    end
end

function tassign(lhs::TExpr, rhs::TExpr)
    if isa(rhs, TAssign)
        throw(DeError("chained assignment is not supported by Devectorize"))
    end
    mode = decide_assign_tmode(lhs, rhs)
    TAssign{typeof(lhs),typeof(rhs)}(lhs, rhs, mode)
end

function topassign(aop::Symbol, lhs::TExpr, rhs::TExpr)
    op = extract_assign_op(TFun{aop}())
    new_rhs = tcall(op, [lhs, rhs])
    tassign(lhs, new_rhs)
end

function tassign(ex::Expr)
    @assert ex.head == :(=)
    @assert length(ex.args) == 2

    lhs = ex.args[1]
    rhs = ex.args[2]
    tassign(texpr(lhs), texpr(rhs))
end

function topassign(ex::Expr)
    topassign(ex.head, texpr(ex.args[1]), texpr(ex.args[2]))
end

##########################################################################
#
#   block construction
#
##########################################################################

function tblock(blk_ex::Expr)

    @assert blk_ex.head == :(block)

    blk = TBlock()
    for e in blk_ex.args
        if isa(e, LineNumberNode) || e.head == (:line)
            continue
        end
        if !(e.head == :(=) || is_opassign(e.head))
            throw(DeError("Each statement in a block must be an assignment or op-assignment."))
        end
        push!(blk.stmts, texpr(e))
    end
    blk
end

##########################################################################
#
#   AST construction:  Expr ==> TExpr
#
##########################################################################

texpr(x::Number) = tnum(x)
texpr(x::Symbol) = tvar(x)

is_empty_tuple(ex::Expr) = ex.head == :(tuple) && isempty(ex.args)

texpr(ex::Expr) =
    ex.head == :(.) ? tqvar(ex) :
    ex.head == :(call) ? tcall(ex) :
    ex.head == :(comparison) ? tcomparison(ex) :
    ex.head == :(ref) ? tref(ex) :
    ex.head == :(=) ? tassign(ex) :
    ex.head == :(block) ? tblock(ex) :
    is_empty_tuple(ex) ? TEmpty() :
    is_opassign(ex.head) ? topassign(ex) :
    throw(DeError("Unrecognized expression: $ex"))


abstract type AbstractLShapedSolver{F, T <: Real, A <: AbstractVector, M <: LQSolver, S <: LQSolver} <: AbstractStructuredModel end

nscenarios(lshaped::AbstractLShapedSolver) = lshaped.nscenarios

# Initialization #
# ======================================================================== #
function init!(lshaped::AbstractLShapedSolver, subsolver::SubSolver)
    # Initialize progress meter
    lshaped.progress.thresh = lshaped.parameters.τ
    # Cap bundle size
    lshaped.parameters.bundle = min(lshaped.parameters.bundle, lshaped.nscenarios)
    # Prepare the master optimization problem
    prepare_master!(lshaped)
    # Finish initialization based on solver traits
    init_subproblems!(lshaped, subsolver)
    init_solver!(lshaped)
end
# ======================================================================== #

# Functions #
# ======================================================================== #
function set_params!(lshaped::AbstractLShapedSolver; kwargs...)
    for (k,v) in kwargs
        setfield!(lshaped.parameters,k,v)
    end
end

function update_solution!(lshaped::AbstractLShapedSolver)
    ncols = decision_length(lshaped.stochasticprogram)
    nb = nbundles(lshaped)
    x = getsolution(lshaped.mastersolver)
    lshaped.mastervector[:] = x[1:ncols+nb]
    lshaped.x[1:ncols] = x[1:ncols]
    lshaped.θs[:] = x[ncols+1:ncols+nb]
    nothing
end

function calculate_estimate(lshaped::AbstractLShapedSolver)
    return lshaped.c⋅lshaped.x + sum(lshaped.θs)
end

function current_objective_value(lshaped::AbstractLShapedSolver,Qs::AbstractVector)
    return lshaped.c⋅lshaped.x + sum(Qs)
end
current_objective_value(lshaped) = current_objective_value(lshaped,lshaped.subobjectives)

function get_objective_value(lshaped::AbstractLShapedSolver)
    if !isempty(lshaped.Q_history)
        return lshaped.Q_history[end]
    else
        return calculate_objective_value(lshaped)
    end
end

function prepare_master!(lshaped::AbstractLShapedSolver{true})
    # θs
    for i = 1:nbundles(lshaped)
        MPB.addvar!(lshaped.mastersolver.lqmodel,-Inf,Inf,0.0)
        if typeof(lshaped.mastersolver.optimsolver) == GurobiSolver
            updatemodel!(lshaped.mastersolver.lqmodel)
        end
        push!(lshaped.mastervector,-1e10)
        push!(lshaped.θs,-1e10)
    end
end

function prepare_master!(lshaped::AbstractLShapedSolver{false})
    # θs
    for i = 1:nbundles(lshaped)
        MPB.addvar!(lshaped.mastersolver.lqmodel,-Inf,Inf,1.0)
        if typeof(lshaped.mastersolver.optimsolver) == GurobiSolver
            updatemodel!(lshaped.mastersolver.lqmodel)
        end
        push!(lshaped.mastervector,-1e10)
        push!(lshaped.θs,-1e10)
    end
end

function resolve_subproblems!(lshaped::AbstractLShapedSolver{F,T}) where {F, T <: Real}
    # Update subproblems
    update_subproblems!(lshaped.subproblems,lshaped.x)
    # Solve sub problems
    cut_bundle = CutBundle(T)
    for subproblem ∈ lshaped.subproblems
        cut::SparseHyperPlane{T} = subproblem()
        if lshaped.parameters.bundle == 1
            add_cut!(lshaped,cut)
            update_objective!(lshaped,cut)
        else
            add_to_bundle!(lshaped,cut_bundle,cut)
            if length(cut_bundle) == lshaped.parameters.bundle
                aggregated_cut = aggregate!(cut_bundle)
                add_cut!(lshaped,aggregated_cut)
                update_objective!(lshaped,aggregated_cut)
                lshaped.subobjectives[aggregated_cut.id] = cut_bundle.q
                cut_bundle.q = zero(T)
            end
        end
    end
    if lshaped.parameters.bundle > 1 && length(cut_bundle) > 0 && cut_bundle.q < Inf
        # Add remaining bundle
        aggregated_cut = aggregate!(cut_bundle)
        add_cut!(lshaped,aggregated_cut)
        update_objective!(lshaped,aggregated_cut)
        lshaped.subobjectives[aggregated_cut.id] = cut_bundle.q
    end
    # Return current objective value
    return current_objective_value(lshaped)
end

function iterate_nominal!(lshaped::AbstractLShapedSolver{F}) where F
    # Resolve all subproblems at the current optimal solution
    Q = resolve_subproblems!(lshaped)
    if Q == Inf && !F
        return :Infeasible
    end
    if Q == -Inf
        return :Unbounded
    end
    lshaped.solverdata.Q = Q
    # Update the optimization vector
    take_step!(lshaped)
    # Resolve master
    try
        solve_problem!(lshaped,lshaped.mastersolver)
    catch
        # Master problem could not be solved for some reason.
        @unpack Q,θ = lshaped.solverdata
        gap = abs(θ-Q)/(abs(Q)+1e-10)
        @warn "Master problem could not be solved, solver returned status $(status(lshaped.mastersolver)). The following relative tolerance was reached: $(@sprintf("%.1e",gap)). Aborting procedure."
        return :StoppedPrematurely
    end
    if status(lshaped.mastersolver) == :Infeasible
        @warn "Master is infeasible. Aborting procedure."
        return :Infeasible
    end
    # Update master solution
    update_solution!(lshaped)
    lshaped.solverdata.θ = calculate_estimate(lshaped)
    # Log progress
    log!(lshaped)
    # Check optimality
    if check_optimality(lshaped)
        # Optimal
        lshaped.solverdata.Q = calculate_objective_value(lshaped,lshaped.x)
        push!(lshaped.Q_history,lshaped.solverdata.Q)
        return :Optimal
    end
    # Project (if applicable)
    project!(lshaped)
    # Check optimality if level sets are used
    if hastrait(lshaped,LV)
        lshaped.solverdata.θ = calculate_estimate(lshaped)
        if check_optimality(lshaped)
            # Optimal
            lshaped.solverdata.Q = calculate_objective_value(lshaped,lshaped.x)
            push!(lshaped.Q_history,lshaped.solverdata.Q)
            # Final log
            if lshaped.parameters.log
                current_gap = gap(lshaped)
                ProgressMeter.update!(lshaped.progress,current_gap,
                                      showvalues = [
                                          ("Objective",Q),
                                          ("Gap",current_gap),
                                          ("Number of cuts",length(lshaped.cuts))
                                      ])
            end
            return :Optimal
        end
    end
    # Just return a valid status for this iteration
    return :Valid
end

function log!(lshaped::AbstractLShapedSolver)
    @unpack Q,θ = lshaped.solverdata
    push!(lshaped.Q_history,Q)
    push!(lshaped.θ_history,θ)
    lshaped.solverdata.iterations += 1

    log_regularization!(lshaped)

    if lshaped.parameters.log
        current_gap = gap(lshaped)
        ProgressMeter.update!(lshaped.progress,current_gap,
                              showvalues = [
                                  ("Objective",Q),
                                  ("Gap",current_gap),
                                  ("Number of cuts",length(lshaped.cuts))
                              ])
    end
end

function log!(lshaped::AbstractLShapedSolver, t::Integer)
    @unpack Q,θ = lshaped.solverdata
    lshaped.Q_history[t] = Q
    lshaped.θ_history[t] = θ

    log_regularization!(lshaped,t)

    if lshaped.parameters.log
        current_gap = gap(lshaped)
        ProgressMeter.update!(lshaped.progress,current_gap,
                              showvalues = [
                                  ("Objective",Q),
                                  ("Gap",current_gap),
                                  ("Number of cuts",length(lshaped.cuts))
                              ])
    end
end

function check_optimality(lshaped::AbstractLShapedSolver)
    @unpack τ = lshaped.parameters
    @unpack θ = lshaped.solverdata
    return θ > -Inf && gap(lshaped) <= τ
end
# ======================================================================== #

# Cut functions #
# ======================================================================== #
active(lshaped::AbstractLShapedSolver, hyperplane::HyperPlane) = active(hyperplane, lshaped.x, lshaped.parameters.τ)
active(lshaped::AbstractLShapedSolver, cut::HyperPlane{OptimalityCut}) = optimal(cut, lshaped.x, lshaped.θs[cut.id], lshaped.parameters.τ)
satisfied(lshaped::AbstractLShapedSolver, hyperplane::HyperPlane) = satisfied(hyperplane, lshaped.x, lshaped.parameters.τ)
satisfied(lshaped::AbstractLShapedSolver, cut::HyperPlane{OptimalityCut}) = satisfied(cut, lshaped.x, lshaped.θs[cut.id], lshaped.parameters.τ)
violated(lshaped::AbstractLShapedSolver, hyperplane::HyperPlane) = !satisfied(lshaped, hyperplane)
gap(lshaped::AbstractLShapedSolver, hyperplane::HyperPlane) = gap(hyperplane, lshaped.x)
gap(lshaped::AbstractLShapedSolver, cut::HyperPlane{OptimalityCut}) = gap(cut, lshaped.x, lshaped.θs[cut.id])

add_cut!(lshaped::AbstractLShapedSolver, cut::HyperPlane) = add_cut!(lshaped, cut, lshaped.subobjectives)

function add_cut!(lshaped::AbstractLShapedSolver, cut::HyperPlane{OptimalityCut}, subobjectives::AbstractVector, Q::Real)
    θ = lshaped.θs[cut.id]
    @unpack τ = lshaped.parameters
    # Update objective
    subobjectives[cut.id] = Q
    # Check if cut gives new information
    if θ > -Inf && abs(θ-Q) <= τ*(1+abs(Q))
        # Optimal with respect to this subproblem
        return false
    end
    # Add optimality cut
    process_cut!(lshaped, cut)
    MPB.addconstr!(lshaped.mastersolver.lqmodel, lowlevel(cut)...)
    if typeof(lshaped.mastersolver.optimsolver) == GurobiSolver
        updatemodel!(lshaped.mastersolver.lqmodel)
    end
    push!(lshaped.cuts, cut)
    return true
end
add_cut!(lshaped::AbstractLShapedSolver, cut::HyperPlane{OptimalityCut}, subobjectives::AbstractVector, x::AbstractVector) = add_cut!(lshaped, cut,subobjectives, cut(x))
add_cut!(lshaped::AbstractLShapedSolver, cut::HyperPlane{OptimalityCut}, subobjectives::AbstractVector) = add_cut!(lshaped, cut, subobjectives, lshaped.x)

function add_cut!(lshaped::AbstractLShapedSolver, cut::HyperPlane{FeasibilityCut}, subobjectives::AbstractVector, Q::Real)
    # Ensure that there is no false convergence
    subobjectives[cut.id] = Q
    # Add feasibility cut
    process_cut!(lshaped, cut)
    MPB.addconstr!(lshaped.mastersolver.lqmodel, lowlevel(cut)...)
    if typeof(lshaped.mastersolver.optimsolver) == GurobiSolver
        updatemodel!(lshaped.mastersolver.lqmodel)
    end
    push!(lshaped.cuts, cut)
    return true
end
add_cut!(lshaped::AbstractLShapedSolver, cut::HyperPlane{FeasibilityCut}, subobjectives::AbstractVector) = add_cut!(lshaped, cut, subobjectives, Inf)

function add_cut!(lshaped::AbstractLShapedSolver, cut::HyperPlane{Infeasible}, subobjectives::AbstractVector)
    @warn "Subproblem $cut.id is infeasible, procedure will abort."
    subobjectives[cut.id] = Inf
    return true
end

function add_cut!(lshaped::AbstractLShapedSolver, cut::HyperPlane{Unbounded}, subobjectives::AbstractVector)
    @warn "Subproblem $cut.id is unbounded, procedure will abort."
    subobjectives[cut.id] = -Inf
    return true
end

function add_to_bundle!(lshaped::AbstractLShapedSolver, bundle::CutBundle, cut::HyperPlane)
    add_cut!(lshaped, cut)
    bundle.q += cut(lshaped.x)
end

function add_to_bundle!(lshaped::AbstractLShapedSolver, bundle::CutBundle, cut::HyperPlane{OptimalityCut})
    push!(bundle.cuts, cut)
    bundle.q += cut(lshaped.x)
end

update_objective!(lshaped::AbstractLShapedSolver, cut::HyperPlane) = nothing
function update_objective!(lshaped::AbstractLShapedSolver{true}, cut::HyperPlane{OptimalityCut})
    # Ensure that θi is included in minimization if feasibility cuts are used
    c = MPB.getobj(lshaped.mastersolver.lqmodel)
    if c[length(lshaped.x)+cut.id] == 0.0
        c[length(lshaped.x)+cut.id] = 1.0
        MPB.setobj!(lshaped.mastersolver.lqmodel,c)
    end
end

function show(io::IO, lshaped::AbstractLShapedSolver)
    println(io, typeof(lshaped).name.name)
    println(io, "State:")
    show(io, lshaped.solverdata)
    println(io, "Parameters:")
    show(io, lshaped.parameters)
end

function show(io::IO, ::MIME"text/plain", lshaped::AbstractLShapedSolver)
    show(io, lshaped)
end
# ======================================================================== #

# Plot recipe #
# ======================================================================== #
@recipe f(lshaped::AbstractLShapedSolver) = lshaped, -1
@recipe function f(lshaped::AbstractLShapedSolver, time::Real; showθ = false)
    length(lshaped.Q_history) > 0 || error("No solution data. Has solver been run?")
    showθ && (length(lshaped.θ_history) > 0 || error("No solution data. Has solver been run?"))
    Qmin = showθ ? minimum(lshaped.θ_history) : minimum(lshaped.Q_history)
    Qmax = maximum(lshaped.Q_history)
    increment = std(lshaped.Q_history)

    linewidth --> 4
    linecolor --> :black
    tickfontsize := 14
    tickfontfamily := "sans-serif"
    guidefontsize := 16
    guidefontfamily := "sans-serif"
    titlefontsize := 22
    titlefontfamily := "sans-serif"
    xlabel := time == -1 ? "Iteration" : "Time [s]"
    ylabel := "Q"
    ylims --> (Qmin-increment,Qmax+increment)
    if time == -1
        xlims --> (1, length(lshaped.Q_history)+1)
        xticks --> 1:5:length(lshaped.Q_history)
    else
        xlims --> (0, time)
        xticks --> linspace(0, time, ceil(Int,length(lshaped.Q_history)/5))
    end
    yticks --> Qmin:increment:Qmax
    xformatter := (d) -> @sprintf("%.1f",d)
    yformatter := (d) -> begin
        if abs(d) <= sqrt(eps())
            "0.0"
        elseif (log10(abs(d)) < -2.0 || log10(abs(d)) > 3.0)
            @sprintf("%.4e",d)
        elseif log10(abs(d)) > 2.0
            @sprintf("%.1f",d)
        else
            @sprintf("%.2f",d)
        end
    end

    @series begin
        label --> "Q"
        seriescolor --> :black
        if time == -1
            1:1:length(lshaped.Q_history), lshaped.Q_history
        else
            linspace(0,time,length(lshaped.Q_history)), lshaped.Q_history
        end
    end

    if showθ
        @series begin
            label --> "θ"
            linestyle --> :dash
            seriescolor --> :red
            linecolor --> :red
            linewidth --> 2
            if time == -1
                1:1:length(lshaped.θ_history), lshaped.θ_history
            else
                linspace(0,time,length(lshaped.θ_history)), lshaped.θ_history
            end
        end
    end
end
# ======================================================================== #


 using Test
 using LogicCircuits
 using LogicCircuits: SddMgrInnerNode, SddMgrLeafNode

for (V, VI, VL) in [(PlainVtree, PlainVtreeInnerNode, PlainVtreeLeafNode),
                    (SddMgr, SddMgrInnerNode, SddMgrLeafNode)]

    @testset "$V IO test" begin

        vtree = zoo_vtree("little_4var.vtree", V)

        function test_vtree(vtree)
            @test num_variables(vtree) == 4
            @test sort(Int.(variables(vtree))) == [1,2,3,4]
            @test vtree isa VI
            @test left_most_descendent(vtree) isa VL
            @test right_most_descendent(vtree) isa VL
        end

        test_vtree(vtree)

        mktempdir() do tmp
            # Save the vtree
            temp_path = "$tmp/little_4var_temp.vtree"
            write(temp_path, vtree)

            # load from file, and then run the same tests
            vtree2 = read(temp_path, V)
            test_vtree(vtree2)
            @test PlainVtree(vtree) == PlainVtree(vtree2) # we can test equality of plain vtrees!

            mapping = read(temp_path, Dict{String,V})
            for i = 0:num_nodes(vtree)-1 # ids of vtree nodes start at 0
                @test haskey(mapping, "$i")
            end
            @test !haskey(mapping, "$(num_nodes(vtree))")

            # Save dot file
            dot_path = "$tmp/little_4var_temp.dot"
            write(dot_path, vtree)

            # Save unsupported format
            dot_path = "$tmp/little_4var_temp.bad_extension"
            @test_throws String write(dot_path, vtree)
        end

        vtree = zoo_vtree("easy/C17_mince.min.vtree", V)
        @test num_nodes(vtree) == 33
        @test num_variables(vtree) == 17
        @test vtree isa V

    end
end