function opf_model_add_variables(opfmodel::JuMP.Model, opfdata::OPFData,
                                 modelinfo::ModelInfo,
                                 algparams::AlgParams,
                                 kIdx::Int = 0, tIdx::Int = 0)
    # shortcuts for compactness
    buses = opfdata.buses
    gens  = opfdata.generators
    ngen  = length(gens)
    nbus  = length(buses)
    nline = length(opfdata.lines)
    T = modelinfo.num_time_periods
    K = modelinfo.num_ctgs + 1 # base case counted separately
    @assert T >= 1
    @assert K >= 1
    @assert algparams.mode ∈ [:nondecomposed, :lyapunov_bound] || (tIdx >= 1 && kIdx >= 1)
    @assert algparams.mode ∉ [:nondecomposed, :lyapunov_bound] || (tIdx == 0 && kIdx == 0)
    @assert algparams.mode ∈ [:nondecomposed, :lyapunov_bound] || algparams.decompCtgs || kIdx == 1

    # Variables
    @variable(opfmodel, Pg[1:ngen,1:K,1:T])
    @variable(opfmodel, Qg[1:ngen,1:K,1:T])
    @variable(opfmodel, Vm[1:nbus,1:K,1:T])
    @variable(opfmodel, Va[1:nbus,1:K,1:T])
    @variable(opfmodel, ωt[1:K,1:T])        # frequency
    @variable(opfmodel, St[1:ngen,1:T])     # slack for ramp constraints
    @variable(opfmodel, Zt[1:ngen,1:T])     # slack for ramp constraints to guarantee ADMM converges
    @variable(opfmodel, Sk[1:ngen,1:K,1:T]) # slack for ctgs_link_constr
    @variable(opfmodel, Zk[1:ngen,1:K,1:T]) # slack for ctgs_link_constr to guarantee ADMM converges
    if modelinfo.allow_constr_infeas
        @variable(opfmodel, sigma_real[1:nbus,1:K,1:T], start = 0)
        @variable(opfmodel, sigma_imag[1:nbus,1:K,1:T], start = 0)
        @variable(opfmodel, sigma_lineFr[1:nline,1:K,1:T] >= 0, start = 0)
        @variable(opfmodel, sigma_lineTo[1:nline,1:K,1:T] >= 0, start = 0)
    end

    # Variable bounds and start values
    for i=1:ngen
        set_lower_bound.(Pg[i,:,:], gens[i].Pmin)
        set_upper_bound.(Pg[i,:,:], gens[i].Pmax)
        set_start_value.(Pg[i,:,:], 0.5*(gens[i].Pmax + gens[i].Pmin))
        set_lower_bound.(Qg[i,:,:], gens[i].Qmin)
        set_upper_bound.(Qg[i,:,:], gens[i].Qmax)
        set_start_value.(Qg[i,:,:], 0.5*(gens[i].Qmax + gens[i].Qmin))
    end

    fix.(Va[opfdata.bus_ref,:,:], buses[opfdata.bus_ref].Va)
    for i=1:nbus
        set_lower_bound.(Vm[i,:,:], buses[i].Vmin)
        set_upper_bound.(Vm[i,:,:], buses[i].Vmax)
        set_start_value.(Vm[i,:,:], 0.5*(buses[i].Vmax + buses[i].Vmin))
        set_start_value.(Va[i,:,:], buses[opfdata.bus_ref].Va)
    end
    fix.(Zt[:,1], 0)
    fix.(Zk[:,1,:], 0)
    set_start_value.(Zt, 0)
    set_start_value.(Zk, 0)
    for k=2:K
        set_lower_bound.(ωt[k,:], -1)
        set_upper_bound.(ωt[k,:], +1)
        set_start_value.(ωt[k,:], 0)
    end
    for i=1:ngen
        set_lower_bound.(St[i,:], 0)
        set_upper_bound.(St[i,:], 2gens[i].ramp_agc)
        set_start_value.(St[i,:],  gens[i].ramp_agc)
        set_lower_bound.(Sk[i,:,:], 0)
        set_upper_bound.(Sk[i,:,:], 2gens[i].scen_agc)
        set_start_value.(Sk[i,:,:],  gens[i].scen_agc)
    end
    if algparams.mode ∈ [:nondecomposed, :lyapunov_bound]
        fix.(ωt[1,:], 0)
        fix.(St[:,1], 0; force = true)
        fix.(Sk[:,1,:], 0; force = true)
        if modelinfo.time_link_constr_type == :inequality && algparams.mode == :nondecomposed
            fix.(St, 0; force = true)
        end
    else
        if kIdx > 1 || tIdx == 1
            fix.(St[:,1], 0; force = true)
        end
        if !algparams.decompCtgs || kIdx == 1
            fix.(ωt[1,:], 0)
            fix.(Sk[:,1,:], 0; force = true)
        end
    end
    if modelinfo.ctgs_link_constr_type ∉ [:frequency_equality, :frequency_penalty]
        fix.(ωt, 0; force = true)
    end
    if modelinfo.time_link_constr_type != :penalty
        fix.(Zt, 0; force = true)
    end
    if modelinfo.ctgs_link_constr_type ∈ [:frequency_equality, :frequency_penalty, :preventive_equality, :preventive_penalty] ||
        (modelinfo.ctgs_link_constr_type == :corrective_inequality && (algparams.mode == :nondecomposed || !algparams.decompCtgs))
        fix.(Sk, 0; force = true)
    end
    if modelinfo.ctgs_link_constr_type ∉ [:frequency_penalty, :preventive_penalty, :corrective_penalty]
        fix.(Zk, 0; force = true)
    end
end

function opf_model_add_block_constraints(opfmodel::JuMP.Model, opfdata::OPFData, rawdata::RawData, modelinfo::ModelInfo)
    T = modelinfo.num_time_periods
    K = (modelinfo.num_ctgs + 1)
    if modelinfo.allow_constr_infeas
        @views for t=1:T, k=1:K
            opfdata_c = (k == 1) ? opfdata :
                opf_loaddata(rawdata;
                    lineOff = opfdata.lines[rawdata.ctgs_arr[k - 1]],
                    time_horizon_start = t,
                    time_horizon_end = t,
                    load_scale = modelinfo.load_scale,
                    ramp_scale = modelinfo.ramp_scale,
                    corr_scale = modelinfo.corr_scale)
            opf_model_add_real_power_balance_constraints(opfmodel, opfdata_c, opfmodel[:Pg][:,k,t], opfdata.Pd[:,t], opfmodel[:Vm][:,k,t], opfmodel[:Va][:,k,t], opfmodel[:sigma_real][:,k,t])
            opf_model_add_imag_power_balance_constraints(opfmodel, opfdata_c, opfmodel[:Qg][:,k,t], opfdata.Qd[:,t], opfmodel[:Vm][:,k,t], opfmodel[:Va][:,k,t], opfmodel[:sigma_imag][:,k,t])
            if modelinfo.allow_line_limits
                opf_model_add_line_power_constraints(opfmodel, opfdata_c, opfmodel[:Vm][:,k,t], opfmodel[:Va][:,k,t], opfmodel[:sigma_lineFr][:,k,t], opfmodel[:sigma_lineTo][:,k,t])
            end
        end
    else
        zb = zeros(length(opfdata.buses))
        zl = zeros(length(opfdata.lines))
        @views for t=1:T, k=1:K
            opfdata_c = (k == 1) ? opfdata :
                opf_loaddata(rawdata;
                    lineOff = opfdata.lines[rawdata.ctgs_arr[k - 1]],
                    time_horizon_start = t,
                    time_horizon_end = t,
                    load_scale = modelinfo.load_scale,
                    ramp_scale = modelinfo.ramp_scale,
                    corr_scale = modelinfo.corr_scale)
            opf_model_add_real_power_balance_constraints(opfmodel, opfdata_c, opfmodel[:Pg][:,k,t], opfdata.Pd[:,t], opfmodel[:Vm][:,k,t], opfmodel[:Va][:,k,t], zb)
            opf_model_add_imag_power_balance_constraints(opfmodel, opfdata_c, opfmodel[:Qg][:,k,t], opfdata.Qd[:,t], opfmodel[:Vm][:,k,t], opfmodel[:Va][:,k,t], zb)
            if modelinfo.allow_line_limits
                opf_model_add_line_power_constraints(opfmodel, opfdata_c, opfmodel[:Vm][:,k,t], opfmodel[:Va][:,k,t], zl, zl)
            end
        end
    end
    return nothing
end

function opf_model_add_line_power_constraints(opfmodel::JuMP.Model, opfdata::OPFData, Vm, Va, sigma_lineFr, sigma_lineTo)
    # Network data short-hands
    baseMVA = opfdata.baseMVA
    buses = opfdata.buses
    lines = opfdata.lines
    busIdx = opfdata.BusIdx
    YffR,YffI,YttR,YttI,YftR,YftI,YtfR,YtfI,YshR,YshI = computeAdmitances(lines, buses, baseMVA)

    for l in 1:length(lines)
        line = lines[l]
        from = busIdx[line.from]
        to = busIdx[line.to]
        if line.rateA ≈ 0 || line.rateA >= 1.0e10
            continue
        end
        flowmax=(line.rateA/baseMVA)^2

        #branch apparent power limits (from bus)
        Yff_abs2=YffR[l]^2+YffI[l]^2; Yft_abs2=YftR[l]^2+YftI[l]^2
        Yre=YffR[l]*YftR[l]+YffI[l]*YftI[l]; Yim=-YffR[l]*YftI[l]+YffI[l]*YftR[l]
        @NLconstraint(opfmodel,
            Vm[from]^2 *
            ( Yff_abs2*Vm[from]^2 + Yft_abs2*Vm[to]^2
            + 2*Vm[from]*Vm[to]*(Yre*cos(Va[from]-Va[to])-Yim*sin(Va[from]-Va[to]))
            )
            - flowmax
            - (sigma_lineFr[l]/baseMVA)
            <=0
        )

        #branch apparent power limits (to bus)
        Ytf_abs2=YtfR[l]^2+YtfI[l]^2; Ytt_abs2=YttR[l]^2+YttI[l]^2
        Yre=YtfR[l]*YttR[l]+YtfI[l]*YttI[l]; Yim=-YtfR[l]*YttI[l]+YtfI[l]*YttR[l]
        @NLconstraint(opfmodel,
            Vm[to]^2 *
            ( Ytf_abs2*Vm[from]^2 + Ytt_abs2*Vm[to]^2
            + 2*Vm[from]*Vm[to]*(Yre*cos(Va[from]-Va[to])-Yim*sin(Va[from]-Va[to]))
            )
            - flowmax
            - (sigma_lineTo[l]/baseMVA)
            <=0
        )
    end
end

function opf_model_add_real_power_balance_constraints(opfmodel::JuMP.Model, opfdata::OPFData, Pg, Pd, Vm, Va, sigma_real)
    # Network data short-hands
    baseMVA = opfdata.baseMVA
    buses = opfdata.buses
    busIdx = opfdata.BusIdx
    BusGeners = opfdata.BusGenerators
    Ybus = opfdata.Ybus

    rows = Ybus.rowval
    yvals = Ybus.nzval
    g_ij = real.(yvals)
    b_ij = imag.(yvals)

    # Power Balance Equations
    for b in 1:length(buses)
        #real part
        b_start, b_end = Ybus.colptr[b], Ybus.colptr[b+1]-1
        @NLconstraint(opfmodel,
            Vm[b] * sum(Vm[rows[c]] * (g_ij[c] * cos(Va[b] - Va[rows[c]]) + b_ij[c] * sin(Va[b] - Va[rows[c]])) for c in b_start:b_end)
            - ( sum(baseMVA*Pg[g] for g in get(BusGeners, b, Int[])) - Pd[b] + sigma_real[b]) / baseMVA      # Sbus part
            ==0
        )
    end
end

function opf_model_add_imag_power_balance_constraints(opfmodel::JuMP.Model, opfdata::OPFData, Qg, Qd, Vm, Va, sigma_imag)
    # Network data short-hands
    baseMVA = opfdata.baseMVA
    buses = opfdata.buses
    busIdx = opfdata.BusIdx
    BusGeners = opfdata.BusGenerators
    Ybus = opfdata.Ybus

    rows = Ybus.rowval
    yvals = Ybus.nzval
    g_ij = real.(yvals)
    b_ij = imag.(yvals)

    # Power Balance Equations
    for b in 1:length(buses)
        #imaginary part
        b_start, b_end = Ybus.colptr[b], Ybus.colptr[b+1]-1
        @NLconstraint(opfmodel,
            Vm[b] * sum(Vm[rows[c]] * (g_ij[c] * sin(Va[b] - Va[rows[c]]) - b_ij[c] * cos(Va[b] - Va[rows[c]])) for c in b_start:b_end)
            - ( sum(baseMVA*Qg[g] for g in get(BusGeners, b, Int[])) - Qd[b] + sigma_imag[b]) / baseMVA      #Sbus part
            ==0
        )
    end
end

function opf_model_add_time_linking_constraints(opfmodel::JuMP.Model, opfdata::OPFData, modelinfo::ModelInfo)
    (ngen, K, T) = size(opfmodel[:Pg])

    if T > 1
        link = compute_time_linking_constraints(opfmodel, opfdata, modelinfo)

        if modelinfo.time_link_constr_type == :inequality
            @constraint(opfmodel, ramping_p[g=1:ngen,t=2:T], link[:ramping_p][g,t] <= 0)
            @constraint(opfmodel, ramping_n[g=1:ngen,t=2:T], link[:ramping_n][g,t] <= 0)
        else
            @constraint(opfmodel, ramping[g=1:ngen,t=2:T], link[:ramping][g,t] == 0)
        end
    end

    return nothing
end

function opf_model_add_ctgs_linking_constraints(opfmodel::JuMP.Model, opfdata::OPFData, modelinfo::ModelInfo)
    (ngen, K, T) = size(opfmodel[:Pg])

    if K > 1
        link = compute_ctgs_linking_constraints(opfmodel, opfdata, modelinfo)

        if modelinfo.ctgs_link_constr_type == :corrective_inequality
            @constraint(opfmodel, ctgs_p[g=1:ngen,k=2:K,t=1:T], link[:ctgs_p][g,k,t] <= 0)
            @constraint(opfmodel, ctgs_n[g=1:ngen,k=2:K,t=1:T], link[:ctgs_p][g,k,t] <= 0)
        else
            @constraint(opfmodel, ctgs[g=1:ngen,k=2:K,t=1:T], link[:ctgs][g,k,t] == 0)
        end
    end

    return nothing
end

function compute_objective_function(
    opfdict,
    opfdata::OPFData,
    modelinfo::ModelInfo,
    algparams::AlgParams,
    kIdx::Int, tIdx::Int
)
    Pg = opfdict[:Pg]
    ωt = opfdict[:ωt]
    Zt = opfdict[:Zt]
    Zk = opfdict[:Zk]

    (ngen, K, T) = size(Pg)
    k = algparams.decompCtgs ? min(kIdx, K) : kIdx
    t = min(tIdx, T)
    @assert 1 <= t <= T
    @assert 1 <= k <= K

    if kIdx == 1 && modelinfo.allow_obj_gencost
        baseMVA = opfdata.baseMVA
        gens = opfdata.generators
        obj_gencost = sum(gens[g].coeff[gens[g].n-2]*(baseMVA*Pg[g,k,t])^2
                        + gens[g].coeff[gens[g].n-1]*(baseMVA*Pg[g,k,t])
                        + gens[g].coeff[gens[g].n  ]
                        for g=1:ngen)
    else
        obj_gencost = 0
    end
    if modelinfo.allow_constr_infeas
        sigma_real = opfdict[:sigma_real]
        sigma_imag = opfdict[:sigma_imag]
        sigma_lineFr = opfdict[:sigma_lineFr]
        sigma_lineTo = opfdict[:sigma_lineTo]
        nbus = size(sigma_real, 1)
        nline = size(sigma_lineFr, 1)
        obj_constr_infeas = 0.5*sum(sigma_real[b,k,t]^2 + sigma_imag[b,k,t]^2 for b=1:nbus) +
                            0.5*sum(sigma_lineFr[l,k,t]^2 + sigma_lineTo[l,k,t]^2 for l=1:nline)
        obj_constr_infeas = (kIdx > 1 ? modelinfo.weight_ctgs : 1.0)*obj_constr_infeas
    else
        obj_constr_infeas = 0
    end
    if modelinfo.ctgs_link_constr_type ∈ [:frequency_penalty, :frequency_equality]
        obj_freq_ctrl = 0.5*ωt[k,t]^2
    else
        obj_freq_ctrl = 0
    end
    if kIdx == 1 && modelinfo.time_link_constr_type == :penalty
        obj_bigM_penalty_time = 0.5*sum(Zt[:,t].^2)
    else
        obj_bigM_penalty_time = 0
    end
    if modelinfo.ctgs_link_constr_type ∈ [:frequency_penalty, :preventive_penalty, :corrective_penalty]
        obj_bigM_penalty_ctgs = 0.5*sum(Zk[:,k,t].^2)
    else
        obj_bigM_penalty_ctgs = 0
    end

    return modelinfo.obj_scale*(
            obj_gencost +
            (modelinfo.weight_constr_infeas*obj_constr_infeas) +
            (modelinfo.weight_freq_ctrl*obj_freq_ctrl) +
            (algparams.θ_t*obj_bigM_penalty_time) +
            (algparams.θ_c*obj_bigM_penalty_ctgs)
        )
end

function compute_time_linking_constraints(
    opfdict,
    opfdata::OPFData,
    modelinfo::ModelInfo,
    tIdx::Int = 0
)
    Pg = opfdict[:Pg]
    St = opfdict[:St]
    Zt = opfdict[:Zt]

    gens = opfdata.generators
    (ngen, K, T) = size(Pg)
    Trange = (tIdx == 0) ? (1:T) : (tIdx:tIdx)

    link = Dict{Symbol, Array}()
    if modelinfo.time_link_constr_type == :inequality
        link[:ramping_p] = [(t > 1) ? (+Pg[g,1,t-1] - Pg[g,1,t] - gens[g].ramp_agc) : 0.0 for g=1:ngen,t=Trange]
        link[:ramping_n] = [(t > 1) ? (-Pg[g,1,t-1] + Pg[g,1,t] - gens[g].ramp_agc) : 0.0 for g=1:ngen,t=Trange]
    elseif modelinfo.time_link_constr_type == :equality
        link[:ramping] = [(t > 1) ? (Pg[g,1,t-1] - Pg[g,1,t] + St[g,t] - gens[g].ramp_agc) : 0.0 for g=1:ngen,t=Trange]
    else
        link[:ramping] = [(t > 1) ? (Pg[g,1,t-1] - Pg[g,1,t] + St[g,t] + Zt[g,t] - gens[g].ramp_agc) : 0.0 for g=1:ngen,t=Trange]
    end

    return link
end

function compute_ctgs_linking_constraints(
    opfdict,
    opfdata::OPFData,
    modelinfo::ModelInfo,
    kIdx::Int = 0, tIdx::Int = 0
)
    Pg = opfdict[:Pg]
    ωt = opfdict[:ωt]
    Sk = opfdict[:Sk]
    Zk = opfdict[:Zk]

    gens = opfdata.generators
    (ngen, K, T) = size(Pg)
    Trange = (tIdx == 0) ? (1:T) : (tIdx:tIdx)
    Krange = (kIdx == 0) ? (1:K) : (kIdx:kIdx)

    link = Dict{Symbol, Array}()
    if modelinfo.ctgs_link_constr_type == :corrective_inequality
        link[:ctgs_p] = [(k > 1) ? (+Pg[g,1,t] - Pg[g,k,t] - gens[g].scen_agc) : 0.0 for g=1:ngen,k=Krange,t=Trange]
        link[:ctgs_n] = [(k > 1) ? (-Pg[g,1,t] + Pg[g,k,t] - gens[g].scen_agc) : 0.0 for g=1:ngen,k=Krange,t=Trange]
    elseif modelinfo.ctgs_link_constr_type == :corrective_equality
        link[:ctgs] = [(k > 1) ? (Pg[g,1,t] - Pg[g,k,t] + Sk[g,k,t] - gens[g].scen_agc) : 0.0 for g=1:ngen,k=Krange,t=Trange]
    elseif modelinfo.ctgs_link_constr_type == :corrective_penalty
        link[:ctgs] = [(k > 1) ? (Pg[g,1,t] - Pg[g,k,t] + Sk[g,k,t] + Zk[g,k,t] - gens[g].scen_agc) : 0.0 for g=1:ngen,k=Krange,t=Trange]
    elseif modelinfo.ctgs_link_constr_type == :preventive_equality
        link[:ctgs] = [(k > 1) ? (Pg[g,1,t] - Pg[g,k,t]) : 0.0 for g=1:ngen,k=Krange,t=Trange]
    elseif modelinfo.ctgs_link_constr_type == :preventive_penalty
        link[:ctgs] = [(k > 1) ? (Pg[g,1,t] - Pg[g,k,t] + Zk[g,k,t]) : 0.0 for g=1:ngen,k=Krange,t=Trange]
    elseif modelinfo.ctgs_link_constr_type == :frequency_equality
        link[:ctgs] = [(k > 1) ? (Pg[g,1,t] - Pg[g,k,t] + (gens[g].alpha*ωt[k,t])) : 0.0 for g=1:ngen,k=Krange,t=Trange]
    else
        link[:ctgs] = [(k > 1) ? (Pg[g,1,t] - Pg[g,k,t] + (gens[g].alpha*ωt[k,t]) + Zk[g,k,t]) : 0.0 for g=1:ngen,k=Krange,t=Trange]
    end

    return link
end

function compute_quadratic_penalty(
    opfdict,
    opfdata::OPFData,
    opfBlockData::OPFBlocks, blk::Int,
    modelinfo::ModelInfo,
    algparams::AlgParams
)
    (ngen, K, T) = size(opfdict[:Pg])
    block = opfBlockData.blkIndex[blk]
    k = block[1]
    t = block[2]

    if t > 1 && k == 1
        inequality = (modelinfo.time_link_constr_type == :inequality)
        inequality && (modelinfo.time_link_constr_type = :equality)
        link = compute_time_linking_constraints(opfdict, opfdata, modelinfo, t)
        inequality && (modelinfo.time_link_constr_type = :inequality)
        lyapunov_quadratic_penalty_time = sum(link[:ramping][:].^2)
    else
        lyapunov_quadratic_penalty_time = 0
    end

    if k > 1 && algparams.decompCtgs
        inequality = (modelinfo.ctgs_link_constr_type == :corrective_inequality)
        inequality && (modelinfo.ctgs_link_constr_type = :corrective_equality)
        link = compute_ctgs_linking_constraints(opfdict, opfdata, modelinfo, k, t)
        inequality && (modelinfo.ctgs_link_constr_type = :corrective_inequality)

        lyapunov_quadratic_penalty_ctgs = sum(link[:ctgs][:].^2)
    else
        lyapunov_quadratic_penalty_ctgs = 0
    end

    return ((0.5algparams.ρ_t*lyapunov_quadratic_penalty_time) +
            (0.5algparams.ρ_c*lyapunov_quadratic_penalty_ctgs))
end

function compute_quadratic_penalty(
    opfdict,
    opfdata::OPFData,
    modelinfo::ModelInfo,
    algparams::AlgParams
)
    (ngen, K, T) = size(opfdict[:Pg])

    if T > 1
        inequality = (modelinfo.time_link_constr_type == :inequality)
        inequality && (modelinfo.time_link_constr_type = :equality)
        link = compute_time_linking_constraints(opfdict, opfdata, modelinfo)
        inequality && (modelinfo.time_link_constr_type = :inequality)

        lyapunov_quadratic_penalty_time = sum(link[:ramping][:,2:T].^2)
    else
        lyapunov_quadratic_penalty_time = 0
    end

    if K > 1 && algparams.decompCtgs
        inequality = (modelinfo.ctgs_link_constr_type == :corrective_inequality)
        inequality && (modelinfo.ctgs_link_constr_type = :corrective_equality)
        link = compute_ctgs_linking_constraints(opfdict, opfdata, modelinfo)
        inequality && (modelinfo.ctgs_link_constr_type = :corrective_inequality)

        lyapunov_quadratic_penalty_ctgs = sum(link[:ctgs][:,2:K,:].^2)
    else
        lyapunov_quadratic_penalty_ctgs = 0
    end

    return ((0.5algparams.ρ_t*lyapunov_quadratic_penalty_time) +
            (0.5algparams.ρ_c*lyapunov_quadratic_penalty_ctgs))
end

function compute_lagrangian_function(
    opfdict, λ::OPFDualSolution,
    opfdata::OPFData,
    opfBlockData::OPFBlocks, blk::Int,
    modelinfo::ModelInfo,
    algparams::AlgParams
)

    (ngen, K, T) = size(opfdict[:Pg])
    block = opfBlockData.blkIndex[blk]
    k = block[1]
    t = block[2]

    obj = compute_objective_function(opfdict, opfdata, opfBlockData, blk, modelinfo, algparams)

    if t > 1 && k == 1
        link = compute_time_linking_constraints(opfdict, opfdata, modelinfo, t)
        lagrangian_t = sum(λ.ramping[:,t].*link[:ramping][:])
    else
        lagrangian_t = 0
    end

    if k > 1 && algparams.decompCtgs
        link = compute_ctgs_linking_constraints(opfdict, opfdata, modelinfo, k, t)
        lagrangian_c = sum(λ.ctgs[:,k,t].*link[:ctgs][:])
    else
        lagrangian_c = 0
    end

    return obj + lagrangian_t + lagrangian_c
end

function compute_proximal_function(
    x1::OPFPrimalSolution, x2::OPFPrimalSolution,
    opfBlockData::OPFBlocks, blk::Int,
    modelinfo::ModelInfo,
    algparams::AlgParams
)
    block = opfBlockData.blkIndex[blk]
    k = block[1]
    t = block[2]

    prox_pg = algparams.τ*sum((x1.Pg[:,k,t] .- x2.Pg[:,k,t]).^2)
    prox_penalty = 0

    if modelinfo.time_link_constr_type == :penalty
        if t > 1 && k == 1
            # Note: algparams.ρ_t/32.0 is the prox weight for the zt variables
            prox_penalty += (algparams.ρ_t/32.0)*sum((x1.Zt[:,t] .- x2.Zt[:,t]).^2)
        end
    end

    if k > 1 && algparams.decompCtgs
        # Note: algparams.ρ_c/32.0 is the prox weight for the zk variables
        if modelinfo.ctgs_link_constr_type ∈ [:frequency_penalty, :preventive_penalty, :corrective_penalty]
            prox_penalty += (algparams.ρ_c/32.0)*sum((x1.Zk[:,k,t] .- x2.Zk[:,k,t]).^2)
        end
    end

    return 0.5*(prox_pg + prox_penalty)
end

function compute_objective_function(
    opfdict, opfdata::OPFData,
    opfBlockData::OPFBlocks, blk::Int,
    modelinfo::ModelInfo, algparams::AlgParams
)
    K = size(opfdict[:Pg],2)
    block = opfBlockData.blkIndex[blk]
    t = block[2]
    if K > 1 && algparams.decompCtgs
        k = block[1]
        return compute_objective_function(opfdict, opfdata, modelinfo, algparams, k, t)
    else
        return sum(compute_objective_function(opfdict, opfdata, modelinfo, algparams, k, t) for k=1:K)
    end
end

function compute_objective_function(
    opfdict, opfdata::OPFData,
    modelinfo::ModelInfo, algparams::AlgParams
)
    (_, K, T) = size(opfdict[:Pg])
    return sum(compute_objective_function(opfdict, opfdata, modelinfo, algparams, k, t) for k=1:K, t=1:T)
end

function compute_objective_function(
    x::OPFPrimalSolution, opfdata::OPFData,
    opfBlockData::OPFBlocks, blk::Int,
    modelinfo::ModelInfo, algparams::AlgParams
)
    d = Dict(:Pg => x.Pg,
             :ωt => x.ωt,
             :Zt => x.Zt,
             :Zk => x.Zk,
             :sigma_real => x.sigma_real,
             :sigma_imag => x.sigma_imag,
             :sigma_lineFr => x.sigma_lineFr,
             :sigma_lineTo => x.sigma_lineTo)
    return compute_objective_function(d, opfdata, opfBlockData, blk, modelinfo, algparams)
end

function compute_lyapunov_function(
    x::OPFPrimalSolution, λ::OPFDualSolution, opfdata::OPFData,
    opfBlockData::OPFBlocks, blk::Int,
    xref::OPFPrimalSolution,
    modelinfo::ModelInfo,
    algparams::AlgParams
)
    d = Dict(:Pg => x.Pg,
             :ωt => x.ωt,
             :St => x.St,
             :Zt => x.Zt,
             :Sk => x.Sk,
             :Zk => x.Zk,
             :sigma_real => x.sigma_real,
             :sigma_imag => x.sigma_imag,
             :sigma_lineFr => x.sigma_lineFr,
             :sigma_lineTo => x.sigma_lineTo)
    lagrangian = compute_lagrangian_function(d, λ, opfdata, opfBlockData, blk, modelinfo, algparams)
    quadratic_penalty = compute_quadratic_penalty(d, opfdata, opfBlockData, blk, modelinfo, algparams)
    proximal = compute_proximal_function(x, xref, opfBlockData, blk, modelinfo, algparams)

    return lagrangian + quadratic_penalty + (0.5*proximal)
end

function compute_dual_error(
    x::OPFPrimalSolution, xprev::OPFPrimalSolution,
    λ::OPFDualSolution, λprev::OPFDualSolution,
    opfdata::OPFData,
    opfBlockData::OPFBlocks, blk::Int,
    modelinfo::ModelInfo, algparams::AlgParams;
    lnorm = Inf
)
    (ngen, K, T) = size(x.Pg)
    k = opfBlockData.blkIndex[blk][1]
    t = opfBlockData.blkIndex[blk][2]

    err_pg = zeros(ngen)
    err_ωt = 0.0
    err_st = zeros(ngen)
    err_sk = zeros(ngen)

    # prox errors
    err_pg .-= algparams.τ*(x.Pg[:,k,t] .- xprev.Pg[:,k,t])

    # time linking errors
    if k == 1
        # for convenience
        β = [g.ramp_agc for g in opfdata.generators]
        if t > 1
            temp_arr = -λ.ramping[:,t] .+ λprev.ramping[:,t] .+
                        algparams.ρ_t*(
                            (algparams.jacobi ? xprev.Pg[:,1,t-1] : x.Pg[:,1,t-1]) .-
                            x.Pg[:,1,t] .+ x.St[:,t] .+ xprev.Zt[:,t] .- β
                        )
            err_pg .+= temp_arr
            err_st .+= -temp_arr
        end
        if t < T
            err_pg .+= λ.ramping[:,t+1] .- λprev.ramping[:,t+1] .-
                        algparams.ρ_t*(
                            x.Pg[:,1,t] .- xprev.Pg[:,1,t+1] .+ xprev.St[:,t+1] .+ xprev.Zt[:,t+1] .- β
                        )
        end
    end

    # ctgs linking errors
    if K > 1 && algparams.decompCtgs
        # for convenience
        β = zeros(ngen)
        if modelinfo.ctgs_link_constr_type ∈ [:corrective_inequality, :corrective_equality, :corrective_penalty]
            for g in 1:ngen
                β[g] = opfdata.generators[g].scen_agc
            end
        end

        if k == 1
            err_pg .+= dropdims(sum(
                            λ.ctgs[:,2:K,t] - λprev.ctgs[:,2:K,t] .-
                            algparams.ρ_c*(
                                -xprev.Pg[:,2:K,t] .+ xprev.Sk[:,2:K,t] .+ xprev.Zk[:,2:K,t]
                            ); dims = 2); dims = 2) .+
                        (algparams.ρ_c*(K-1)*(-x.Pg[:,1,t] .+ β)) .-
                        [algparams.ρ_c*g.alpha*sum(xprev.ωt[2:K,t]) for g in opfdata.generators]
        end

        if k > 1
            temp_arr = -λ.ctgs[:,k,t] .+ λprev.ctgs[:,k,t] .+
                        algparams.ρ_c*(
                            (algparams.jacobi ? xprev.Pg[:,1,t] : x.Pg[:,1,t]) .-
                            x.Pg[:,k,t] .+ x.Sk[:,k,t] .+ xprev.Zk[:,k,t] .- β .+
                            [g.alpha*x.ωt[k,t] for g in opfdata.generators]
                        )
            err_pg .+= temp_arr
            if modelinfo.ctgs_link_constr_type ∈ [:corrective_penalty]
                err_sk .+= -temp_arr
            end
            if modelinfo.ctgs_link_constr_type ∈ [:frequency_penalty]
                err_ωt += -sum(opfdata.generators[g].alpha * temp_arr[g] for g in 1:ngen)
            end
        end
    end

    return norm([err_pg; err_ωt; err_st; err_sk], lnorm)
end

function compute_dual_error(
    x::OPFPrimalSolution, xprev::OPFPrimalSolution,
    λ::OPFDualSolution, λprev::OPFDualSolution,
    opfdata::OPFData,
    modelinfo::ModelInfo, algparams::AlgParams;
    lnorm = Inf
)
    (ngen, K, T) = size(x.Pg)

    err_pg = zeros(ngen, K, T)
    err_ωt = zeros(K, T)
    err_st = zeros(ngen, T)
    err_sk = zeros(ngen, K, T)

    if T > 1
        @assert modelinfo.time_link_constr_type == :penalty
        @views begin
            # for convenience
            β = zeros(ngen, T)
            for g=1:ngen
                β[g,:] .= opfdata.generators[g].ramp_agc
            end

            #----------------------------------------------------------------------------
            true_pg_dual = -λ.ramping[:,2:T]
            lagrangian_pg_err = -λprev.ramping[:,2:T]
            penalty_pg_forward_err = -algparams.ρ_t*(
                                        (algparams.jacobi ? xprev.Pg[:,1,1:(T-1)] : x.Pg[:,1,1:(T-1)]) .-
                                        x.Pg[:,1,2:T] .+ x.St[:,2:T] .+ xprev.Zt[:,2:T] .- β[:,2:T]
                                    )
            penalty_pg_reverse_err = +algparams.ρ_t*(
                                        x.Pg[:,1,1:(T-1)] .- xprev.Pg[:,1,2:T] .+ xprev.St[:,2:T] .+ xprev.Zt[:,2:T] .- β[:,2:T]
                                    )
            prox_pg_err = algparams.τ*(x.Pg[:,1,:] .- xprev.Pg[:,1,:])
            #----------------------------------------------------------------------------

            err_pg[:,1,1:(T-1)] += -true_pg_dual .+ lagrangian_pg_err .- penalty_pg_reverse_err
            err_pg[:,1,2:T] += true_pg_dual .- lagrangian_pg_err .- penalty_pg_forward_err
            err_pg[:,1,1:T] -= prox_pg_err[:,1:T]
            err_st[:,2:T] += penalty_pg_forward_err
            err_st[:,2:T] += -true_pg_dual .+ lagrangian_pg_err
        end
    end

    if K > 1 && algparams.decompCtgs
        @assert modelinfo.ctgs_link_constr_type ∈ [:frequency_penalty, :preventive_penalty, :corrective_penalty]
        @views begin
            # for convenience
            β = zeros(ngen, K, T)
            pg_base = zeros(ngen, K, T)
            pg_base_prev = zeros(ngen, K, T)
            if modelinfo.ctgs_link_constr_type ∈ [:corrective_inequality, :corrective_equality, :corrective_penalty]
                for g=1:ngen
                    β[g,:,:] .= opfdata.generators[g].scen_agc
                end
            end
            for k=2:K, g=1:ngen
                pg_base[g,k,:] .= x.Pg[g,1,:]
                pg_base_prev[g,k,:] .= xprev.Pg[g,1,:]
            end

            #----------------------------------------------------------------------------
            true_pg_ctgs_dual = -λ.ctgs[:,2:K,:]
            lagrangian_pg_ctgs_err = -λprev.ctgs[:,2:K,:]
            penalty_pg_base_err = pg_base[:,2:K,:] .-
                                        xprev.Pg[:,2:K,:] .+ xprev.Sk[:,2:K,:] .+ xprev.Zk[:,2:K,:] .- β[:,2:K,:]
            penalty_pg_ctgs_err = (algparams.jacobi ? pg_base_prev[:,2:K,:] : pg_base[:,2:K,:]) -
                                            x.Pg[:,2:K,:] .+     x.Sk[:,2:K,:] .+ xprev.Zk[:,2:K,:] .- β[:,2:K,:]
            prox_pg_err = algparams.τ*(x.Pg[:,2:K,:] .- xprev.Pg[:,2:K,:])
            for g=1:ngen
                penalty_pg_ctgs_err[g,:,:] += opfdata.generators[g].alpha*x.ωt[2:K,:]
                penalty_pg_base_err[g,:,:] += opfdata.generators[g].alpha*xprev.ωt[2:K,:]
            end
            penalty_pg_base_err .= algparams.ρ_c*penalty_pg_base_err
            penalty_pg_ctgs_err .= -algparams.ρ_c*penalty_pg_ctgs_err
            #----------------------------------------------------------------------------

            err_pg[:,1,:] += dropdims(sum(-true_pg_ctgs_dual .+ lagrangian_pg_ctgs_err .- penalty_pg_base_err; dims = 2); dims = 2)
            err_pg[:,2:K,:] += true_pg_ctgs_dual .- lagrangian_pg_ctgs_err .- penalty_pg_ctgs_err .- prox_pg_err
            if modelinfo.ctgs_link_constr_type ∈ [:corrective_penalty]
                err_sk[:,2:K,:] += penalty_pg_ctgs_err - true_pg_ctgs_dual .+ lagrangian_pg_ctgs_err
            end
            if modelinfo.ctgs_link_constr_type ∈ [:frequency_penalty]
                err_ωt[2:K,:] += sum(opfdata.generators[g].alpha * (-true_pg_ctgs_dual[g,:,:] .+ lagrangian_pg_ctgs_err[g,:,:] .+ penalty_pg_ctgs_err[g,:,:]) for g=1:ngen)
            end
        end
    end

    err_pg_view = view(err_pg, :, :, :)
    err_ωt_view = view(err_ωt, :, :)
    err_st_view = view(err_st, :, :)
    err_sk_view = view(err_sk, :, :, :)

    dual_error = CatView(err_pg_view, err_ωt_view, err_st_view, err_sk_view)

    return norm(dual_error, lnorm)
end

function compute_true_ramp_error(
    x::OPFPrimalSolution,
    opfdata::OPFData,
    modelinfo::ModelInfo,
    tIdx::Int = 0
)
    d = Dict(:Pg => x.Pg,
             :St => x.St,
             :Zt => x.Zt)
    if modelinfo.time_link_constr_type == :inequality
        link = compute_time_linking_constraints(d, opfdata, modelinfo, tIdx)
        return maximum(max.(link[:ramping_p], link[:ramping_n], 0.0))
    end

    true_time_link_constr_type = modelinfo.time_link_constr_type
    modelinfo.time_link_constr_type = :equality
    link = compute_time_linking_constraints(d, opfdata, modelinfo, tIdx)
    modelinfo.time_link_constr_type = true_time_link_constr_type

    return maximum(abs.(link[:ramping]))
end

function compute_true_ctgs_error(
    x::OPFPrimalSolution,
    opfdata::OPFData,
    modelinfo::ModelInfo,
    kIdx::Int = 0, tIdx::Int = 0
)
    d = Dict(:Pg => x.Pg,
             :ωt => x.ωt,
             :Sk => x.Sk,
             :Zk => x.Zk)
    if modelinfo.ctgs_link_constr_type == :corrective_inequality
        link = compute_ctgs_linking_constraints(d, opfdata, modelinfo, kIdx, tIdx)
        return maximum(max.(abs.(link[:ctgs_p][:]), abs.(link[:ctgs_n][:]), 0.0))
    end

    true_ctgs_link_constr_type = modelinfo.ctgs_link_constr_type
    if true_ctgs_link_constr_type ∈ [:corrective_penalty]
        modelinfo.ctgs_link_constr_type = :corrective_equality
    end
    if true_ctgs_link_constr_type ∈ [:preventive_penalty]
        modelinfo.ctgs_link_constr_type = :preventive_equality
    end
    if true_ctgs_link_constr_type ∈ [:frequency_penalty]
        modelinfo.ctgs_link_constr_type = :frequency_equality
    end
    link = compute_ctgs_linking_constraints(d, opfdata, modelinfo, kIdx, tIdx)
    modelinfo.ctgs_link_constr_type = true_ctgs_link_constr_type

    return maximum(abs.(link[:ctgs][:]))
end

function compute_true_ramp_error(
    x::OPFPrimalSolution, opfdata::OPFData,
    opfBlockData::OPFBlocks, blk::Int,
    modelinfo::ModelInfo,
)
    block = opfBlockData.blkIndex[blk]
    k = block[1]
    t = block[2]
    if k > 1
        return 0.0
    end
    return compute_true_ramp_error(x, opfdata, modelinfo, t)
end

function compute_true_ctgs_error(
    x::OPFPrimalSolution, opfdata::OPFData,
    opfBlockData::OPFBlocks, blk::Int,
    modelinfo::ModelInfo,
)
    block = opfBlockData.blkIndex[blk]
    k = block[1]
    t = block[2]
    return compute_true_ctgs_error(x, opfdata, modelinfo, k, t)
end

"""
    opf_block_get_auglag_penalty_expr(
        opfmodel::JuMP.Model,
        modelinfo::ModelInfo,
        opfdata::OPFData,
        k::Int, t::Int,
        algparams::AlgParams,
        primal::OPFPrimalSolution,
        dual::OPFDualSolution
    )

Let `k` and `t` denote the contingency number and time period of
the NLP `block`. Then, depending on `algparams.decompCtgs`,
this function must return an appropriate expression.

We use ``\\mathbb{I}[...]`` to denote the indicator function.
Also, unless otherwise indicated,
- ``z_g`` are parameters and must be taken from `primal.Zt`
- ``p_g`` and ``s_g`` variables that are not indexed with `t`
    are parameters and must be taken from `primal.Pg` and `primal.St`
- ``\\lambda`` (without `k` subscript) must be taken from `dual.ramping`
- ``\\rho_t`` must be taken from `algparams.ρ_t`
- ``\\tau`` must be taken from `algparams.τ`

* If `algparams.decompCtgs == false`, then this function must return:
```math
\\begin{aligned}
\\sum_{g \\in G} \\Bigg\\{
& 0.5\\tau [p^0_{g,t} - \\mathrm{primal}.p^0_{g,t}]^2 \\\\
&+\\mathbb{I}[t > 1]\\Big(
\\lambda_{gt}[p^0_{g,t-1} - p^0_{g,t} + s_{g,t} + z_{g,t} - r_g] +
0.5\\rho_{t}[p^0_{g,t-1} - p^0_{g,t} + s_{g,t} + z_{g,t} - r_g]^2
\\Big) \\\\
&+\\mathbb{I}[t < T]\\Big(
\\lambda_{g,t+1}[p^0_{g,t} - p^0_{g,t+1} + s_{g,t+1} + z_{g,t+1} - r_g] +
0.5\\rho_{t}[p^0_{g,t} - p^0_{g,t+1} + s_{g,t+1} + z_{g,t+1} - r_g]^2
\\Big) \\Bigg\\}
\\end{aligned}
```

* If `algparams.decompCtgs == true`, then this function must return:
(to do)

"""
function opf_block_get_auglag_penalty_expr(
    opfmodel::JuMP.Model,
    modelinfo::ModelInfo,
    opfdata::OPFData,
    k::Int, t::Int,
    algparams::AlgParams,
    primal::OPFPrimalSolution,
    dual::OPFDualSolution
)

    (ngen, K, T) = size(primal.Pg)
    gens = opfdata.generators
    @assert t >= 1 && t <= T
    @assert k >= 1 && k <= K
    @assert modelinfo.num_time_periods == 1
    @assert modelinfo.num_ctgs >= 0
    @assert k == 1 || algparams.decompCtgs

    # get variables
    Pg = opfmodel[:Pg]
    ωt = opfmodel[:ωt]
    St = opfmodel[:St]
    Sk = opfmodel[:Sk]

    auglag_penalty = @expression(opfmodel, 0.5*algparams.τ*sum((Pg[g,1,1] - primal.Pg[g,k,t])^2 for g=1:ngen))

    if !algparams.decompCtgs || k == 1
        if t > 1
            ramp_link_expr_prev =
                @expression(opfmodel,
                    [g=1:ngen],
                        primal.Pg[g,1,t-1] - Pg[g,1,1] + St[g,1] + primal.Zt[g,t] - gens[g].ramp_agc
                )
            auglag_penalty += sum(  dual.ramping[g,t]*(+ramp_link_expr_prev[g])   +
                                    0.5*algparams.ρ_t*(+ramp_link_expr_prev[g])^2
                                for g=1:ngen)
        end
        if t < T
            ramp_link_expr_next =
                @expression(opfmodel,
                    [g=1:ngen],
                        Pg[g,1,1] - primal.Pg[g,1,t+1] + primal.St[g,t+1] + primal.Zt[g,t+1] - gens[g].ramp_agc
                )
            auglag_penalty += sum(  dual.ramping[g,t+1]*(+ramp_link_expr_next[g]) +
                                    0.5*algparams.ρ_t*(+ramp_link_expr_next[g])^2
                                for g=1:ngen)
        end
    end

    if algparams.decompCtgs && K > 1
        @assert modelinfo.num_ctgs == 0
        β = [gens[g].scen_agc for g=1:ngen]
        (modelinfo.ctgs_link_constr_type ∉ [:corrective_inequality, :corrective_equality, :corrective_penalty]) &&
            (β .= 0)

        if k > 1
            ctgs_link_expr_prev =
                @expression(opfmodel,
                    [g=1:ngen],
                        primal.Pg[g,1,t] - Pg[g,1,1] + (gens[g].alpha*ωt[1,1]) + Sk[g,1,1] + primal.Zk[g,k,t] - β[g]
                )
            auglag_penalty += sum(    dual.ctgs[g,k,t]*(+ctgs_link_expr_prev[g]) +
                                    0.5*algparams.ρ_c*(+ctgs_link_expr_prev[g])^2
                                for g=1:ngen)
        else
            ctgs_link_expr_next =
                @expression(opfmodel,
                    [g=1:ngen,j=2:K],
                        Pg[g,1,1] - primal.Pg[g,j,t] + (gens[g].alpha*primal.ωt[j,t]) + primal.Sk[g,j,t] + primal.Zk[g,j,t] - β[g]
                )
            auglag_penalty += sum(     dual.ctgs[g,j,t]*(+ctgs_link_expr_next[g,j]) +
                                    0.5*algparams.ρ_c*(+ctgs_link_expr_next[g,j])^2
                                for j=2:K, g=1:ngen)
        end
    end

    drop_zeros!(auglag_penalty)

    return auglag_penalty
end


"""
    add_objective!(
        community::CoreModel,
        objective_mets::Vector{String};
        objective_weights = Float64[],
        objective_column_index = 0,
    )

Add an objective to the `community` model. Supply the string names of the
objective metabolites in `objective_mets`. Optionally specify the weight to
assign each metabolite in the objective function, if unassigned then equal
weight is assumed. Also, optionally specify whether the objective already exists
in the model by assigning `objective_column_index`. If unassigned then an
objective column will be added, otherwise the column at `objective_column_index`
will be updated.

Note, the weights are negated inside the function so that the objective metabolites
are seen as reagents/substrates, not products in the reaction equation.

# Example
```
add_objective!(model, ["met1", "met2"]) # adds a new column with weights = [1,1]
add_objective!(model, ["met1", "met2"]; objective_weights=[0.1, 0.9]) # adds a new column
add_objective!(model, ["met1", "met2"]; objective_weights=[0.1, 0.9], objective_column_index=10) # updates column 10
```
"""
function add_objective!(
    community::CoreModel,
    objective_mets::Vector{String};
    objective_weights = Float64[],
    objective_column_index = 0,
)
    obj_inds = indexin(objective_mets, metabolites(community))
    if isempty(objective_weights)
        objective_weights = repeat([1.0], inner = length(objective_mets))
    end

    if objective_column_index == 0 # needs to be created
        nr, _ = size(community.S)
        objcol = spzeros(nr)
        objcol[obj_inds] .= -objective_weights

        # extend model by one reaction
        community.S = hcat(community.S, objcol)
        community.xl = [community.xl; 0.0]
        community.xu = [community.xu; 1000.0]
        community.rxns = [community.rxns; "community_biomass"]
        community.c = spzeros(size(community.S, 2))
        community.c[end] = 1.0
    else # only update
        community.S[:, objective_column_index] .= 0.0 # reset
        community.S[obj_inds, objective_column_index] .= -objective_weights
        dropzeros!(community.S)
        community.c = spzeros(size(community.S, 2))
        community.c[objective_column_index] = 1.0
        community.xl[objective_column_index] = 0.0
        community.xu[objective_column_index] = 1000.0
    end
end

"""
    add_model_with_exchanges(
        community::CoreModel,
        model::MetabolicModel,
        exchange_rxn_ids::Vector{String},
        exchange_met_ids::Vector{String};
        model_name = "unknown_species",
        biomass_id = nothing,
    )::CoreModel

Add `model` to `community`, which is a pre-existing community model with
`exchange_rxn_ids` and `exchange_met_ids`. The `model_name` is appended to
each reaction and metabolite, see [`join`](@ref). If `biomass_id` is specified
then a biomass metabolite for `model` is also added to the resulting model. The
column corresponding to the `biomass_id` reaction then produces this new biomass
metabolite with unit coefficient. Note, `exchange_rxn_ids` and
`exchange_met_ids` must already exist in the `community` model.

# Example
```
community = add_model_with_exchanges(community, model, exchange_rxn_ids, exchange_met_ids; model_name="species_2", biomass_id="BIOMASS_Ecoli_core_w_GAM")
```
"""
function add_model_with_exchanges(
    community::CoreModel,
    model::MetabolicModel,
    exchange_rxn_ids::Vector{String},
    exchange_met_ids::Vector{String};
    model_name = "unknown_species",
    biomass_id = nothing,
)::CoreModel

    exchange_met_community_inds = indexin(exchange_met_ids, metabolites(community))
    exchange_rxn_community_inds = indexin(exchange_rxn_ids, reactions(community))
    if any(isnothing.(exchange_met_community_inds)) ||
       any(isnothing.(exchange_rxn_community_inds))
        throw(
            DomainError(
                "exchange metabolite/reaction not found.",
                "Exchange metabolites/reactions must already be contained in the community model.",
            ),
        )
    end

    n_cmodel_rows, n_cmodel_cols = size(stoichiometry(community))
    n_model_rows, n_model_cols = size(stoichiometry(model))
    # A note on the variable names here.Suppose M is some sparse matrix, then I
    # = row indices, J = column indices and V = values at the associated
    # indices. So I[a] = i, J[a]=j and then M[i,j] = V[a]
    Iadd, Jadd, Vadd = findnz(stoichiometry(model))

    # shift to fit into community
    Iadd .+= n_cmodel_rows
    Jadd .+= n_cmodel_cols

    # when adding a single model not that many reactions, push! okay?
    exchange_rxn_model_idxs = indexin(exchange_rxn_ids, reactions(model))
    for i = 1:length(exchange_rxn_ids)
        isnothing(exchange_rxn_model_idxs[i]) && continue
        push!(Iadd, exchange_met_community_inds[i]) # already present ex met in community model
        push!(Jadd, n_cmodel_cols + exchange_rxn_model_idxs[i]) # new column hence the offset
        push!(Vadd, 1.0)
    end

    biomass_met = 0.0
    if biomass_id != "" # add biomass metabolite
        biomass_rxn = first(indexin([biomass_id], reactions(model)))
        push!(Iadd, n_model_rows + n_cmodel_rows + 1)
        push!(Jadd, biomass_rxn + n_cmodel_cols)
        push!(Vadd, 1.0)
        biomass_met = 1
    end

    n_metabolites_total = n_model_rows + n_cmodel_rows + biomass_met
    n_reactions_total = n_cmodel_cols + n_model_cols

    I, J, V = findnz(stoichiometry(community))
    I = [I; Iadd]
    J = [J; Jadd]
    V = [V; Vadd]
    S = sparse(I, J, V, n_metabolites_total, n_reactions_total)

    # A note on the variables here. The bounds are vectors of upper and lower
    # bounds for each reaction. So lbs = [lb_1, lb_2, lb_i, ...], ubs = [ub_1,
    # ub_2, ub_i, ...] for reaction i. See the bounds function for more
    # information
    lbsadd, ubsadd = bounds(model)
    lbs, ubs = bounds(community)
    lbs = [lbs; lbsadd]
    ubs = [ubs; ubsadd]

    rxnsadd = "$(model_name)_" .* reactions(model)
    if !isnothing(biomass_id)
        metsadd =
            ["$(model_name)_" .* metabolites(model); "$(model_name)_" * biomass_id]
    else
        metsadd = "$(model_name)_" .* metabolites(model)
    end
    rxns = [reactions(community); rxnsadd]
    mets = [metabolites(community); metsadd]

    # adds to the original community data, could possibly reset?
    I, V = findnz(balance(community))
    b = sparsevec(I, V, n_metabolites_total)
    I, V = findnz(objective(community))
    c = sparsevec(I, V, n_reactions_total)

    return CoreModel(S, b, c, lbs, ubs, rxns, mets)
end

"""
    join_with_exchanges(models::Vector{M},
        exchange_rxn_ids::Vector{String},
        exchange_met_ids::Vector{String};
        add_biomass_objective=false,
        biomass_ids::Vector{String},
        model_names=String[]
    )

Return a `CoreModel` representing the community model of `models` joined through
their `exchange_rxn_ids` and `exchange_met_ids`. These exchange reactions and
metabolites link to environmental metabolites and reactions.
Optionally specify `model_names` to append a specific name to each reaction
and metabolite of an organism for easier reference (default is `species_i` for
each model index i in `models`). Note, the bounds of the environmental variables
are all set to zero. Thus, to run a simulation you need to constrain them
appropriately. All the other bounds are inherited from the models used to
construct the community model.

If `add_biomass_objective` is true then `biomass_ids` needs to be supplied as
well. This creates a model with an extra reaction added to the end of the
stoichiometric matrix (last column) that can be assigned as the objective
reaction. It also creates biomass "metabolites" that can be used in this
objective reaction. Note, this reaction is unspecified, further action needs to
be taken to specify it, e.g. assign weights to the last column of the
stoichiometric matrix in the rows corresponding to the biomass metabolites.

To further clarify how this `join` works. Suppose you have 2 organisms with
stoichiometric matrices S₁ and S₂ and you want to link them with
`exchange_met_ids = [em₁, em₂, em₃, ...]` and `exchange_rxn_ids = [er₁, er₂,
er₃, ...]`. Then a new community stoichiometric matrix is constructed that looks
like this:
```
            _      er₁  er₂  er₃  ...  b_
           |S₁                           |
           |   S₂                        |
        em₁|                             |
S   =   em₂|                             |
        em₃|                             |
        ...|                             |
        bm₁|                             |
        bm₂|_                           _|

```
The exchange reactions in each model get linked to environmental metabolites,
`emᵢ`, and these get linked to environmental exchanges, `erᵢ`. These `erᵢ`
behave like normal single organism exchange reactions. When
`add_biomass_objective` is true each model's biomass becomes a pseudo-metabolite
(`bmᵢ`). These can be weighted in column `b`, called the `community_biomass`
reaction in the community model, if desired. Refer to the tutorial if this is
unclear.

# Example
```
m1 = load_model(core_model_path)
m2 = load_model(CoreModel, core_model_path)

# need to list ALL the exchanges that will form part of the entire model
exchange_rxn_ids = filter(looks_like_exchange_reaction, reactions(m1))
exchange_met_ids = [first(keys(reaction_stoichiometry(m1, ex_rxn))) for ex_rxn in exchange_rxn_ids]

biomass_ids = ["BIOMASS_Ecoli_core_w_GAM", "BIOMASS_Ecoli_core_w_GAM"]

community = join_with_exchanges(
    [m1, m2],
    exchange_rxn_ids,
    exchange_met_ids;
    add_biomass_objective = true,
    biomass_ids = biomass_ids,
)
```
"""
function join_with_exchanges(
    models::Vector{M},
    exchange_rxn_ids::Vector{String},
    exchange_met_ids::Vector{String};
    add_biomass_objective = true,
    biomass_ids = String[],
    model_names = String[],
)::CoreModel where M <: MetabolicModel

    if add_biomass_objective && isempty(biomass_ids)
        throw(
            DomainError(
                "biomass_ids",
                "Please add supply the string ids of the biomass functions when `add_biomass_objective` is true.",
            ),
        )
    end

    if length(exchange_met_ids) != length(exchange_rxn_ids)
        throw(
            DomainError(
                "Exchange identifiers are misspecified",
                "The lenghts of the exchange metabolite and reactions are different.",
            ),
        )
    end

    # get offsets to construct community S
    reaction_lengths = [n_reactions(model) for model in models]
    metabolite_lengths = [n_metabolites(model) for model in models]
    reaction_offset = [0; cumsum(reaction_lengths[1:end-1])]
    metabolite_offset = [0; cumsum(metabolite_lengths[1:end-1])]

    # get each model's S matrix (needed for the size calculations)
    stoichs = [stoichiometry(model) for model in models]
    nnzs = [findnz(stoich) for stoich in stoichs] # nonzero indices per model

    # size calculations
    column_add = add_biomass_objective ? 1 : 0 # objective rxn
    row_add = add_biomass_objective ? length(models) : 0 # biomass as metabolites
    nnz_add = add_biomass_objective ? (1 + length(models)) : 0
    nnz_total =
        sum(length(first(nnz)) for nnz in nnzs) +
        length(models) * length(exchange_rxn_ids) +
        length(exchange_met_ids) + nnz_add
    n_reactions_metabolic = sum(reaction_lengths)
    n_reactions_total = n_reactions_metabolic + length(exchange_rxn_ids) + column_add
    n_metabolites_metabolic = sum(metabolite_lengths)
    n_metabolites_total = n_metabolites_metabolic + length(exchange_met_ids) + row_add

    # Create empty storage vectors
    lbs = spzeros(n_reactions_total)
    ubs = spzeros(n_reactions_total)
    rxns = Array{String,1}(undef, n_reactions_total)
    mets = Array{String,1}(undef, n_metabolites_total)
    I = Array{Int,1}(undef, nnz_total)
    J = Array{Int,1}(undef, nnz_total)
    V = Array{Float64,1}(undef, nnz_total)

    # build metabolic components, block diagonals
    kstart = 1
    for i = 1:length(models)
        kend = kstart + length(nnzs[i][3]) - 1
        rng = kstart:kend
        I[rng] .= nnzs[i][1] .+ metabolite_offset[i]
        J[rng] .= nnzs[i][2] .+ reaction_offset[i]
        V[rng] .= nnzs[i][3]
        kstart += length(nnzs[i][3])
    end

    # build environmental - exchange links
    for i = 1:length(models)
        exchange_rxn_inds = indexin(exchange_rxn_ids, reactions(models[i]))
        exchange_met_inds = indexin(exchange_met_ids, metabolites(models[i]))
        for (n, (ex_rxn, ex_met)) in enumerate(zip(exchange_rxn_inds, exchange_met_inds)) # each exchange rxn has one exchange met
            isnothing(ex_rxn) && continue
            isnothing(ex_met) && continue
            # connect environmental metabolite with exchange metabolite
            I[kstart] = n_metabolites_metabolic + n
            J[kstart] = ex_rxn + reaction_offset[i]
            V[kstart] = -stoichs[i][ex_met, ex_rxn] # ex is normally negative, make positive
            kstart += 1
        end
    end

    # # build diagonal environmental exchanges
    for i = 1:length(exchange_rxn_ids)
        I[kstart] = n_metabolites_metabolic + i
        J[kstart] = n_reactions_metabolic + i
        V[kstart] = -1.0
        kstart += 1
    end

    if add_biomass_objective
        n_before_biomass_row = n_metabolites_metabolic + length(exchange_met_ids)
        for i = 1:length(models)
            biomass_ind = first(indexin([biomass_ids[i]], reactions(models[i])))
            I[kstart] = i + n_before_biomass_row
            J[kstart] = biomass_ind + reaction_offset[i]
            V[kstart] = 1.0
            kstart += 1
        end
    end

    S = sparse(
        I[1:kstart-1],
        J[1:kstart-1],
        V[1:kstart-1],
        n_metabolites_total,
        n_reactions_total,
    ) # could be that some microbes don't have all the exchanges, hence kstart-1

    _reaction_offsets = cumsum(reaction_lengths)
    _metabolite_offsets = cumsum(metabolite_lengths)
    for i = 1:length(models)
        species = isempty(model_names) ? "species_$(i)" : model_names[i]
        tlbs, tubs = bounds(models[i])
        lbs[reaction_offset[i]+1:_reaction_offsets[i]] .= tlbs
        ubs[reaction_offset[i]+1:_reaction_offsets[i]] .= tubs
        rxns[reaction_offset[i]+1:_reaction_offsets[i]] =
            "$(species)_" .* reactions(models[i])
        mets[metabolite_offset[i]+1:_metabolite_offsets[i]] =
            "$(species)_" .* metabolites(models[i])
    end
    mets[_metabolite_offsets[end]+1:_metabolite_offsets[end]+length(exchange_met_ids)] .=
        exchange_met_ids
    rxns[_reaction_offsets[end]+1:_reaction_offsets[end]+length(exchange_rxn_ids)] .=
        exchange_rxn_ids

    if add_biomass_objective
        rxns[end] = "community_biomass"
        for i = 1:length(models)
            species = isempty(model_names) ? "species_$(i)" : model_names[i]
            mets[end-length(biomass_ids)+i] = "$(species)_" .* biomass_ids[i]
        end
    end

    return CoreModel(S, spzeros(size(S, 1)), spzeros(size(S, 2)), lbs, ubs, rxns, mets)
end


abstract type AbstractMessage end

struct NetworkEnvelope
    magic::UInt32
    command::String
    length::UInt32
    checksum::UInt32
    payload::Vector{UInt8}
    NetworkEnvelope(magic::UInt32, command::AbstractString, length::Integer, checksum::UInt32, payload::Vector{UInt8}) = new(magic, command, length, checksum, payload)
end

NetworkEnvelope(command::AbstractString, payload::Vector{UInt8}, testnet=false) = NetworkEnvelope(NETWORK_MAGIC[testnet], command, length(payload), reinterpret(UInt32, hash256(payload))[1], payload)
NetworkEnvelope(command::AbstractString, payload::AbstractMessage, testnet=false) = NetworkEnvelope(command, serialize(payload), testnet)

function show(io::IO, z::NetworkEnvelope)
    print(io, "Network Envelope - ", z.command, "\n", z.length, " bytes, checksum ", string(z.checksum, base=16), " on network ", string(z.magic, base=16), "\n--------\n", bytes2hex(z.payload))
end

"""
    IOBuffer, Bool -> Vector{NetworkEnvelope}

Takes a stream and creates a NetworkEnvelope
"""
function io2envelopes(s::IOBuffer, testnet::Bool=false)
    result = NetworkEnvelope[]
    while !(bytesavailable(s) < 24)
        try
            global magic = reinterpret(UInt32, read(s, 4))[1]
        catch BoundsError
            error("Connection reset!", bytes2hex(bin))
        end
        if magic != NETWORK_MAGIC[testnet]
            error("magic is not right ", magic, " vs ", NETWORK_MAGIC[testnet])
        end
        command = strip(String(read(s, 12)), '\0')
        payload_length = reinterpret(UInt32, read(s, 4))[1]
        checksum = reinterpret(UInt32, read(s, 4))[1]
        while bytesavailable(s) < payload_length
            println("Awaiting data from remote node...")
            sleep(0.02)
        end
        payload = read(s, payload_length)
        calculated_checksum = reinterpret(UInt32, hash256(payload)[1:4])[1]
        if calculated_checksum != checksum
            println("\n\nResult so far was : ", result,
                    "\nLast message had command ", command,
                    ", payload length ", payload_length,
                    " and payload ", bytes2hex(payload))
            error("Error parsing IO, calculated checksum does not match ", calculated_checksum, " vs ", checksum)
        end
        push!(result, NetworkEnvelope(magic, command, payload_length, checksum, payload))
    end
    result
end

function io2envelopes(bin::Vector{UInt8}, testnet::Bool=false)
    s = IOBuffer(bin)
    io2envelopes(s, testnet)
end

function io2envelope(s::IOBuffer, testnet::Bool=false)
    if !(bytesavailable(s) < 24)
        magic = reinterpret(UInt32, read(s, 4))[1]
        if magic != NETWORK_MAGIC[testnet]
            error("Network magic is not right!\nWe have received ", magic, " versus ", NETWORK_MAGIC[testnet])
        end
        command = strip(String(read(s, 12)), '\0')
        payload_length = reinterpret(UInt32, read(s, 4))[1]
        checksum = reinterpret(UInt32, read(s, 4))[1]
        while bytesavailable(s) < payload_length
            println("Buffer doesn't hold sufficient data to parse payoad, waiting for 5 s...")
            sleep(5)
        end
        payload = read(s, payload_length)
        calculated_checksum = reinterpret(UInt32, hash256(payload)[1:4])[1]
        if calculated_checksum != checksum
            println("\n\nResult so far was : ", result,
            "\nLast message had command ", command,
            ", payload length ", payload_length,
            " and payload ", bytes2hex(payload))
            error("Error parsing IO, calculated checksum does not match ", calculated_checksum, " vs ", checksum)
        end
        return NetworkEnvelope(magic, command, payload_length, checksum, payload)
    end
end

"""
Returns the byte serialization of the entire network message
"""
function serialize(envelope::NetworkEnvelope)
    result = Vector(reinterpret(UInt8, [htol(envelope.magic)]))
    append!(result, UInt8.(collect(envelope.command)))
    append!(result, fill(0x00, (12 - length(envelope.command))))
    append!(result, bytes(length(envelope.payload), len=4, little_endian=true))
    append!(result, bytes(envelope.checksum, len=4, little_endian=true))
    append!(result, envelope.payload)
end

struct Peer
    time::UInt32
    services::UInt64
    ip::IPAddr
    port::UInt16
    Peer(services::Integer, ip::IPAddr, port::Integer, time::Integer=0) = new(time, services, ip, port)
end

Peer(testnet::Bool=false) = Peer(DEFAULT["services"], DEFAULT["ip"], DEFAULT["port"][testnet])
Peer(ip::IPAddr, port::Integer) = Peer(DEFAULT["services"], ip, port)

function show(io::IO, z::Peer)
    print(io, z.services, "@", z.ip, ":", z.port)
end

"""
    IPv4 -> Vector{UInt8}

Return an 16 bytes UInt8 array representing the IP address

IPv6 address. Network byte order. The original client only supported IPv4 and
only read the last 4 bytes to get the IPv4 address. However, the IPv4 address
is written into the message as a 16 byte IPv4-mapped IPv6 address

(12 bytes 00 00 00 00 00 00 00 00 00 00 FF FF, followed by the 4 bytes of the
IPv4 address).
"""
function ip2bytes(ip::IPv4)
    result = fill(0x00, 10)
    append!(result, [0xff, 0xff])
    append!(result, Vector(reinterpret(UInt8, [hton(ip.host)])))
end

function bytes2ip(bin::Vector{UInt8})
    if bin[1:12] == IPV4_PREFIX
        IPv4(ntoh(reinterpret(UInt32, bin[13:16])[1]))
    else
        IPv6(ntoh(reinterpret(UInt128, bin)[1]))
    end
end

"""
    Peer -> Vector{UInt8}

Returns the serialization of a Peer
"""
function serialize(peer::Peer, versionmessage::Bool=false)
    result = UInt8[]
    if !versionmessage
        append!(result, Vector(reinterpret(UInt8, [htol(peer.time)])))
    end
    append!(result, Vector(reinterpret(UInt8, [htol(peer.services)])))
    append!(result, ip2bytes(peer.ip))
    append!(result, Vector(reinterpret(UInt8, [hton(peer.port)])))
end

"""
    parse_peer(payload::Vector{UInt8}) -> Peer

Parse Peer from bytes arrays
"""
function parse_peer(payload::Vector{UInt8}, versionmessage::Bool=false)
    io = IOBuffer(payload)
    if versionmessage
        time = ltoh(reinterpret(UInt32, read(io, 4))[1])
    else
        time = 0
    end
    services = ltoh(reinterpret(UInt64, read(io, 8))[1])
    ip = bytes2ip(read(io, 16))
    port = ntoh(reinterpret(UInt16, read(io, 8))[1])
    Peer(services, ip, port, time)
end

struct VersionMessage <: AbstractMessage
    command::String
    version::UInt32
    services::UInt64
    timestamp::UInt64
    receiver::Peer
    sender::Peer
    nonce::UInt64
    user_agent::String
    start_height::UInt32
    relay::Bool
    VersionMessage(version::Integer, services::Integer, timestamp::Integer, receiver::Peer, sender::Peer, nonce::Integer, user_agent::String, start_height::Integer, relay::Bool) = new("version", version, services, timestamp, receiver, sender, nonce, user_agent, start_height, relay)
end

VersionMessage(timestamp::UInt64, nonce::UInt64, testnet::Bool=false) =
    VersionMessage(DEFAULT["version"],
                   DEFAULT["services"],
                   timestamp,
                   Peer(testnet), Peer(testnet),
                   nonce,
                   USER_AGENT,
                   DEFAULT["start_height"],
                   DEFAULT["relay"])
VersionMessage(testnet::Bool=false) =
    VersionMessage(UInt64(round(time())), rand(UInt64), testnet)

function show(io::IO, z::VersionMessage)
    print(io, "Version Message\n--------\nVersion : ",
            z.version, " - User Agent : ", String(z.user_agent),
            "\nServices : ", z.services, " - Relay : ", z.relay,
            "\nTime Stamp : ", unix2datetime(z.timestamp),
            "\nLatest Block : ", string(z.start_height, base=16),
            "\nReceiver : ", z.receiver,
            "\nSender : ", z.sender)
end

"""
    serialize(version::VersionMessage) -> Vector{UInt8}

Serialize this message to send over the network
"""
function serialize(version::VersionMessage)
    result = Vector(reinterpret(UInt8, [htol(version.version)]))
    append!(result, Vector(reinterpret(UInt8, [htol(version.services)])))
    append!(result, Vector(reinterpret(UInt8, [htol(version.timestamp)])))
    append!(result, serialize(version.receiver, true))
    append!(result, serialize(version.sender, true))
    append!(result, Vector(reinterpret(UInt8, [htol(version.nonce)])))
    append!(result, serialize(VarString(version.user_agent)))
    append!(result, Vector(reinterpret(UInt8, [htol(version.start_height)])))
    version.relay ? append!(result, [0x01]) : append!(result, [0x00])
    return result
end

"""
    serialize(version::VersionMessage) -> Vector{UInt8}

"""
function payload2version(payload::Vector{UInt8})
    io = IOBuffer(payload)
    version = ltoh(reinterpret(UInt32, read(io, 4))[1])
    services = ltoh(reinterpret(UInt64, read(io, 8))[1])
    timestamp = ltoh(reinterpret(UInt64, read(io, 8))[1])
    sender = parse_peer(read(io, 26))
    receiver = parse_peer(read(io, 26))
    nonce = ltoh(reinterpret(UInt64, read(io, 8))[1])
    user_agent = io2varstring(io).str
    start_height = ltoh(reinterpret(UInt32, read(io, 4))[1])
    relay = Bool(read(io, 1)[1])
    VersionMessage(version, services, timestamp, receiver, sender, nonce, user_agent, start_height, relay)
end

struct VerAckMessage <: AbstractMessage
    command::String
    VerAckMessage() = new("verack")
end

payload2verack(payload::Vector{UInt8}) = VerAckMessage()

serialize(::VerAckMessage) = UInt8[]

struct PingMessage <: AbstractMessage
    command::String
    nonce::Vector{UInt8}
    PingMessage(nonce) = new("ping", nonce)
end

payload2ping(payload::Vector{UInt8}) = PingMessage(payload)

serialize(ping::PingMessage) = ping.nonce

struct PongMessage <: AbstractMessage
    command::String
    nonce::Vector{UInt8}
    PongMessage(nonce) = new("pong", nonce)
end

payload2pong(payload::Vector{UInt8}) = PongMessage(payload)

serialize(pong::PongMessage) = pong.nonce

struct GetHeadersMessage <: AbstractMessage
    command::String
    version::UInt32
    num_hashes::Integer
    start_block::Vector{UInt8}
    end_block::Vector{UInt8}
    GetHeadersMessage(version::Integer, num_hashes::Integer, start_block::Vector{UInt8}, end_block::Vector{UInt8}=fill(0x00, 32)) = new("getheaders", version, num_hashes, start_block, end_block)
end

GetHeadersMessage(start_block::Vector{UInt8}) = GetHeadersMessage(DEFAULT["version"], 1, start_block)

"""
Serialize this message to send over the network
    protocol version is 4 bytes little-endian
    number of hashes is a varint
    start block is in little-endian
    end block is also in little-endian
"""
function serialize(getheaders::GetHeadersMessage)
    result = bytes(getheaders.version, len=4, little_endian=true)
    append!(result, encode_varint(getheaders.num_hashes))
    append!(result, reverse!(copy(getheaders.start_block)))
    append!(result, reverse!(copy(getheaders.end_block)))
    return result
end

struct HeadersMessage <: AbstractMessage
    command::String
    headers::Vector{BlockHeader}
    HeadersMessage(headers::Vector{BlockHeader}) = new("headers", headers)
end

"""
    # number of headers is in a varint
    # initialize the headers array
    # loop through number of headers times
    # add a header to the headers array by parsing the stream
    # read the next varint (num_txs)
    # num_txs should be 0 or raise a RuntimeError
"""
function payload2headers(payload::Vector{UInt8})
    io = IOBuffer(payload)
    payload2headers(io)
end

function payload2headers(io::IOBuffer)
    num_headers = read_varint(io)
    headers = BlockHeader[]
    for i in 1:num_headers
        push!(headers, io2blockheader(io))
        num_txs = read_varint(io)
        if num_txs != 0
            error("number of txs not 0")
        end
    end
    return HeadersMessage(headers)
end

mutable struct GetDataMessage <: AbstractMessage
    command::String
    data::Vector{Tuple{Integer,Vector{UInt8}}}
    GetDataMessage(data::Vector{Tuple{Integer,Vector{UInt8}}}=Tuple{Integer,Vector{UInt8}}[]) = new("getdata", data)
end

import Base.append!

function append!(x::GetDataMessage, type::Integer, identifier::Vector{UInt8})
    push!(x.data, (type, identifier))
end

function serialize(x::GetDataMessage)
    result = encode_varint(length(x.data))
    for e in x.data
        append!(result, bytes(e[1], len=4, little_endian=true))
        append!(result, reverse!(copy(e[2])))
    end
    return result
end

mutable struct RejectMessage <: AbstractMessage
    command::String
    message::String
    ccode::Char
    reason::String
    data::Vector{UInt8}
    RejectMessage(message::String, ccode::Char, reason::String, data::Vector{UInt8}) = new("reject", message, ccode, reason, data)
end

function show(io::IO, z::RejectMessage)
    print(io, "Reject Message\n--------\nMessage : ",
            z.message, "\nccode : ", z.ccode,
            "\nReason : ", z.reason, "\n", bytes2hex(z.data))
end

"""
    payload2reject(payload::Vector{UInt8}) -> RejectMessage

Parse RejectMessage from NetworkEnvelope payload
"""
function payload2reject(payload::Vector{UInt8})
    io = IOBuffer(payload)
    message = io2varstring(io).str
    ccode = Char(read(io, 1)[1])
    reason = io2varstring(io).str
    data = read(io)
    RejectMessage(message, ccode, reason, data)
end

mutable struct MerkleBlockMessage <: AbstractMessage
    command::String
    header::BlockHeader
    tx_count::UInt32
    hash_count::Unsigned
    hashes::Vector{Vector{UInt8}}
    flag_byte_count::Unsigned
    flags::Vector{Bool}
    MerkleBlockMessage(header::BlockHeader, tx_count::Integer,
                  hash_count, hashes::Vector{Vector{UInt8}}, flag_byte_count,
                  flags::Vector{Bool}) = new("merkleblock",
                  header, tx_count, hash_count, hashes, flag_byte_count, flags)
end

function show(io::IO, z::MerkleBlockMessage)
    print(io, "Merkle Block Message\n--------\nMessage : ",
            z.header, "\ntx_count : ", z.tx_count,
            " flag_byte_count : ", z.flag_byte_count)
end

"""
    payload2merkleblock(payload::Vector{UInt8}) -> MerkleBlockMessage

Parse MerkleBlockMessage from NetworkEnvelope payload
"""
function payload2merkleblock(payload::Vector{UInt8})
    io = IOBuffer(payload)
    header = io2blockheader(io)
    tx_count = ltoh(reinterpret(UInt32, read(io, 4))[1])
    hash_count = read_varint(io)
    hashes = Vector{UInt8}[]
    for i in 1:hash_count
         push!(hashes, read(io, 32))
    end
    flag_byte_count = read_varint(io)
    flag_byte = read(io, flag_byte_count)
    flags = bytes2flags(flag_byte)
    MerkleBlockMessage(header, tx_count, hash_count, hashes, flag_byte_count, flags)
end

"""

Returns true if MerkleBlockMessage is valid
"""
function is_valid(mb::MerkleBlockMessage)
    tree = MerkleTree(mb.tx_count)
    populate!(tree, mb.flags, mb.hashes)
    root(tree) == mb.header.merkle_root
end

struct FilterAddMessage <: AbstractMessage
    command::String
    element_bytes::Unsigned
    element::Vector{UInt8}
    FilterAddMessage(element_bytes::Unsigned, element::Vector{UInt8}) = new("filteradd", element_bytes, element)
end

struct FilterClearMessage <: AbstractMessage
    command::String
    FilterClearMessage() = new("filterclear")
end

struct FilterLoadMessage <: AbstractMessage
    command::String
    n_filter_bytes::Unsigned
    filter::Vector{Bool}
    n_hash_funcs::UInt32
    n_tweak::UInt32
    n_flags::UInt8
    FilterLoadMessage(n_filter_bytes::Unsigned, filter::Vector{Bool}, n_hash_funcs::UInt32, n_tweak::UInt32, n_flags::UInt8) = new("filterload", n_filter_bytes, filter, n_hash_funcs, n_tweak, n_flags)
end

FilterLoadMessage(bf::BloomFilter, flag::UInt8=0x01) = FilterLoadMessage(bf.size, bf.bit_field, bf.function_count, bf.tweak, flag)

function serialize(msg::FilterLoadMessage)
    payload = encode_varint(msg.n_filter_bytes)
    append!(payload, flags2bytes(msg.filter))
    append!(payload, Vector(reinterpret(UInt8, [htol(msg.n_hash_funcs)])))
    append!(payload, Vector(reinterpret(UInt8, [htol(msg.n_tweak)])))
    append!(payload, msg.n_flags%UInt8)
end

struct SendHeadersMessage <: AbstractMessage
    command::String
    SendHeadersMessage() = new("sendheaders")
end

SendHeadersMessage(::Any) = SendHeadersMessage()

struct InventoryVector
    type::Integer
    hash::Vector{UInt8}
    InventoryVector(type, hash) = new(type, hash)
end

function show(io::IO, z::InventoryVector)
    print(io, DATA_MESSAGE_NAME[z.type],
          " :\n", bytes2hex(z.hash))
end

function payload2inventoryvector(payload::Vector{UInt8})
    type = ltoh(reinterpret(UInt32, payload[1:4])[1])
    hash = payload[5:26]
    InventoryVector(type, hash)
end

struct InvMessage <: AbstractMessage
    command::String
    count::Integer
    inventory::Vector{InventoryVector}
    InvMessage(inv_count::Integer, inventory::Vector{InventoryVector}) = new("inv", inv_count, inventory)
end

function payload2inv(payload::Vector{UInt8})
    io = IOBuffer(payload)
    inv_count = read_varint(io)
    inventory = InventoryVector[]
    for i ∈ 1:inv_count
        entry = payload2inventoryvector(read(io, 26))
        push!(inventory, entry)
    end
    InvMessage(inv_count, inventory)
end

struct SendCmpctMessage <: AbstractMessage
    command::String
    announce::Bool
    version::UInt64
    SendCmpctMessage(announce::Bool, version::UInt64) = new("sendcmpct", announce, version)
end

function payload2sendcmpct(payload::Vector{UInt8})
    annouce = Bool(payload[1])
    version = ltoh(reinterpret(UInt64, payload[2:9])[1])
    SendCmpctMessage(annouce, version)
end

struct FeeFilterMessage <: AbstractMessage
    command::String
    feerate::UInt64
    FeeFilterMessage(feerate::UInt64) = new("feefilter", feerate)
end

function payload2feefilter(payload::Vector{UInt8})
    feerate = ltoh(reinterpret(UInt64, payload[1:8])[1])
    SendCmpctMessage(feerate)
end

struct AddrMessage <: AbstractMessage
    n::Unsigned
    addr::Vector{IPAddr}
    FeeFilterMessage(n::Integer, addr::Vector{IPAddr}) = new("addr", n, addr)
end

function payload2addr(payload::Vector{UInt8})
    io = IOBuffer(payload)
    n = read_varint(io)
    addr = IPAddr[]
    for _ ∈ 1:n
        push!(addr, bytes2ip(read(io, 16)))
    end
    AddrMessage(n, addr)
end

PARSE_PAYLOAD = Dict([
    ("addr", payload2addr),
    ("feefilter", payload2feefilter),
    ("headers", payload2headers),
    ("inv", payload2inv),
    ("merkleblock", payload2merkleblock),
    ("ping", payload2ping),
    ("pong", payload2pong),
    ("reject", payload2reject),
    ("sendcmpct", payload2sendcmpct),
    ("sendheaders", SendHeadersMessage),
    ("tx", payload2tx),
    ("verack", payload2verack),
    ("version", payload2version)
])


using SCS
const SCS_PRIMAL_FACTORY =
    MOI.OptimizerWithAttributes(SCS.Optimizer, MOI.Silent() => true)
const SCS_DUAL_FACTORY = dual_optimizer(SCS_PRIMAL_FACTORY)
const SCS_PRIMAL_OPT = MOI.instantiate(SCS_PRIMAL_FACTORY)
const SCS_DUAL_OPT = MOI.instantiate(SCS_DUAL_FACTORY)

push!(primal_power_cone_factory, SCS_PRIMAL_FACTORY)
push!(dual_power_cone_factory, SCS_DUAL_FACTORY)
push!(dual_power_cone_optimizer, SCS_DUAL_OPT)
push!(primal_power_cone_optimizer, SCS_PRIMAL_OPT)

@testset "SCS Exponential Cone Problems" begin
    list_of_exp_problems = [exp1_test, exp2_test]
    test_strong_duality(list_of_exp_problems, SCS_PRIMAL_FACTORY)
end

@testset "SCS Power Cone Problems" begin
    list_of_pow_problems = [pow1_test, pow2_test]
    test_strong_duality(
        list_of_pow_problems,
        SCS_PRIMAL_FACTORY;
        atol = 1e-3,
        rtol = 1e-3,
    )
end


using FixedSizeArrays, GeometryTypes, Colors, GLAbstraction
import GLAbstraction: opengl_prefix
abstract TranspilerBackend

immutable FuncExpr
    name::Symbol
    slots
    args
    body::Vector
    returntype::DataType
    static_parameters::Tuple
end

immutable Light{T}
    position::Vec{3,T}
    ambient::Vec{3,T}
    diffuse::Vec{3,T}
    diffuse_power::T
    specular::Vec{3,T}
    specular_power::T
end

immutable Shading{T}
    ambient::Vec{3, T}
    specular::Vec{3, T}
    shininess::T
end
function get_lambda(pass, f, types)
    lambda = pass(f, types)
    if isa(lambda, Vector)
        if isempty(lambda)
            args = join(map(t->"::$t", types), ", ")
            error("$f($args) couldn't be found")
        end
        return first(lambda)
    else
        isa(lambda, LambdaInfo) && return lambda
        error("Not sure what's up with returntype of $pass. Returned: $lambda")
    end
end

function FuncExpr(f, types)
    lam_lowered = get_lambda(code_lowered, f, types)
    lam_typed = get_lambda(code_typed, f, types)
    slotnames = Base.lambdainfo_slotnames(lam_lowered)
    slottypes = lam_typed.slottypes
    nargs = length(types)
    slots = [(SlotNumber(i), (replace(name, "@", ""), slottypes[i])) for (i, name) in enumerate(slotnames)]
    ast = Base.uncompressed_ast(lam_lowered)
    name = Symbol(f)
    static_params = tuple(lam_typed.sparam_vals...)
    FuncExpr(name, slots, view(slots, 2:(nargs+1)), ast, lam_typed.rettype, static_params)
end

immutable Declaration
    dependencies::Vector{Declaration}
    source::String
end

type Backend{Identifier} <: TranspilerBackend
    #declarations::Dict{String, Declaration}
    current_slots::Vector{Dict{Slot, Tuple{String, DataType}}}
    type_map::Dict{DataType, String}
    function_map::Dict{Tuple{String, Vector{DataType}}, Tuple{String, DataType}}
    indentation_level::Int
end
type GTBlock
    entry
    block
    out
end
GTBlock(entry) = GTBlock(entry, [], Nullable{GotoNode}())
"""
Transpiled Expression
"""
immutable TExpr
    source::String
    typ::DataType
end
returntype(x::TExpr) = x.typ
source(x::TExpr) = x.source

typealias GL Backend{:GL}

function Backend(sym::Symbol)
    Backend{sym}(
        Dict{Slot, Tuple{String, DataType}}[],
        Dict{DataType, String}(),
        Dict{Tuple{String, Tuple}, Tuple{String, DataType}}(),
        1
    )
end

function insert_function!(backend::GL, jlfun, glsl_fun, types_ret::Pair)
    types, returntype = types_ret
    backend.function_map[(string(jlfun), types)] = (string(glsl_fun), returntype)
end

function insert_buildins!(backend::GL)
    # TODO do this in a cool way!
    # e.g define Module buildins with glsl inline and other tricks
    types = (Float32, Float64, Int32, UInt32)

    type_map = Dict(
        Float32 => "float",
        Float64 => "double",
        Int32   => "int",
        UInt32  => "uint"
    )
    for i=1:4
        for T in (Vec, Point)
            for ET in types
                VT = T{i, ET}
                if i==1
                    type_map[VT] = type_map[ET]
                else
                    type_map[VT] = string(opengl_prefix(ET), "vec", i)
                end
                for j=1:4
                    type_map[Mat{i,j,ET}] = string(
                        opengl_prefix(ET), "mat", i==j ? "$i" : "$(i)x$(j)"
                    )
                end
            end
        end
    end
    type_map[RGB{Float32}] = "vec3"
    type_map[RGBA{Float32}] = "vec4"
    merge!(backend.type_map, type_map)
    buildins = Dict(
        3 => Symbol[:round,:ceil,:trunc,:floor,:fma,:any,:all,:clamp],
        2 => Symbol[:floor,:max,:any,:ldexp,:ceil,:normalize,:all,:log,:mod,:cross,:trunc,:round,:min],
        1 => Symbol[:atanh,:tanh,:exp2,:floor,:max,:any,:sin,:tan,:modf,:step,:log2,:acosh,:ceil,:normalize,:length,:atan,:all,:log,:isnan,:trunc,:asinh,:transpose,:abs,:frexp,:sqrt,:round,:sinh,:sign,:acos,:asin,:cosh,:exp,:min,:isinf,:cos]
    )
    ops = (
        ".*" => "*",
        ".+" => "+",
        "./" => "/",
        "*" => "*",
        "+" => "+",
        "/" => "/",
        # "div_float" => "/",
        # "sub_float" => "-",
        # "add_float" => "+"
    )
    boolfuns = [:all, :any]
    # TODO actually figure out the signature of the GLSL build-in functions
    glsl_types = unique(collect(keys(backend.type_map)))
    for T in glsl_types
        if !isa(T, Texture) && !isa(T, Mat)
            for i=1:3
                for fun in buildins[i]
                    insert_function!(backend, string(fun), string(fun), [T for _=1:i] => T)
                end
            end
            if T <: FixedVector
                insert_function!(backend, "dot", "dot", [T, T] => eltype(T))
            end
            for (jl_op, glsl_op) in ops
                insert_function!(backend, jl_op, glsl_op, [T, T] => T)
            end
            for jl_op in ("*", "-", "+")
                insert_function!(backend, jl_op, jl_op, [T] => T)
            end
        end
    end
    # cases not yet covered generically
    insert_function!(backend, "Float32", "float", [Float32] => Float32)
    insert_function!(backend, "Float32", "float", [Int] => Float32)
    insert_function!(backend, "Float32", "float", [Int32] => Float32)
    insert_function!(backend, "Float32", "float", [UInt] => Float32)
    insert_function!(backend, "Float32", "float", [UInt32] => Float32)

    insert_function!(backend, "norm", "normalize", [Vec3f0] => Float32)
    insert_function!(backend, "^", "pow", [Float32, Float32] => Float32)
    insert_function!(backend, "^", "pow", [Int32, Int32] => Float32)

    insert_function!(backend, Symbol(Vec3f0), "vec3", [Float32, Float32, Float32] => Vec3f0)
    insert_function!(backend, Symbol(Vec3f0), "vec3", [Vec3f0] => Vec3f0)
    insert_function!(backend, "<=", "<=", [Float32, Float32] => Bool)
    insert_function!(backend, ".*", "*", [Vec3f0, Float32] => Vec3f0)
    insert_function!(backend, ".*", "*", [Float32, Vec3f0] => Vec3f0)
    insert_function!(backend, "*", "*", [Vec3f0, Float32] => Vec3f0)
    insert_function!(backend, "*", "*", [Float32, Vec3f0] => Vec3f0)
    insert_function!(GLBackend, "+", "+", [Vec3f0, Vec3f0, Vec3f0] => Vec3f0)

end

const GLBackend = Backend(:GL)
insert_buildins!(GLBackend)

isbox(x) = (x == GlobalRef(Base, :box))
extract_sym(x) = error("$x $(typeof(x)) cant be converted to Symbol")
extract_sym(x::Symbol) = x
extract_sym(x::QuoteNode) = x.value
get_types(x::AbstractVector) = map(slot_name_t -> slot_name_t[2][2], x)

"""
Type parameter are not supported, so we'll put them in the name
"""
function to_type_name(T)
    join((T.name.name, T.parameters...), "_")
end
function to_type_name(backend::GL, T::DataType)
    get!(backend.type_map, T) do
        declare_composite_type(backend, T)
    end
end

function to_type_assert(backend::GL, name::String, typ::String)
    "$typ $name"
end
function to_type_assert(backend::GL, name::String, typ::DataType)
    tn = to_type_name(backend, typ)
    to_type_assert(backend, name, tn)
end

function declare_slots(backend::GL, slots)
    join(map(slots) do slot_name_t
        slot, (name, t) = slot_name_t
        slot_declare = to_type_assert(backend, name, t)
        declare_block(backend, slot_declare)
    end, "")
end

function declare_block(backend::GL, source::String, level=backend.indentation_level)
    repeat("    ", level)*source*";\n"
end
function declare_composite_type(backend::GL, T::DataType)
    name = to_type_name(T)
    get!(backend.type_map, T) do
        fields = [to_type_assert(backend, string(n), fieldtype(T, n)) for n in fieldnames(T)]
        #TODO actualy insert this into declarations
        println("""
struct $(name){
$(join(fields, ";\n"));
};
        """)
        name
    end
end

function declare_function(backend::GL, func::FuncExpr)
    types = get_types(func.args)
    name, rettype = get!(backend.function_map, (string(func.name), types)) do
        arg_decl = map(arg->to_type_assert(backend, arg[2]...), func.args)
        rettype = to_type_name(backend, func.returntype)
        transpilat = transpile(backend, func.body)
        src = source(transpilat)
        arg_end = last(func.args.indexes[1])
        var_decl = declare_slots(backend::GL, func.slots[(arg_end+1):end])
        #@assert returntype(transpilat) == func.returntype
        name = func.name
        #TODO actualy insert this into declarations
        println("""
$rettype $name($(join(arg_decl, ", "))){
$var_decl
$src
}
""")
        string(name), func.returntype
    end
    name, rettype
end

function transpile(backend::GL, f::Symbol, types)
    func = getfield(current_module(), f)
    transpile(backend, func, types)
end
function transpile(backend::GL, f::GlobalRef, types)
    func = getfield(f.mod, f.name)
    transpile(backend, func, types)
end
function transpile(backend::GL, f::Union{Function, Type}, types::AbstractVector)
    transpile(backend, f, tuple(types...))
end

function remove_static_param!(expr, static_parameters)
    expr # leave any other untouched
end
function remove_static_param!(expr::Vector, static_parameters)
    map!(e->remove_static_param!(e, static_parameters), expr)
end
function remove_static_param!(expr::Expr, static_parameters)
    if expr.head == :static_parameter
        idx = expr.args[1]
        return static_parameters[idx]
    else
        remove_static_param!(expr.args, static_parameters)
        expr
    end
end
function transpile(backend::GL, f::Union{Function, Type}, types::Tuple)
    name, rettype = get!(backend.function_map, (string(Symbol(f)), [types...])) do
        func = FuncExpr(f, types)
        # convert slot types to glsl slot types
        map!(func.slots) do slot_name_t
            s, (n, t) = slot_name_t
            if applicable(GLAbstraction.gl_promote, t)
                t = GLAbstraction.gl_promote(t)
            end
            s, (n, t)
        end
        remove_static_param!(func.body, func.static_parameters)
        push!(backend.current_slots, Dict(func.slots))
        decl = declare_function(backend, func)
        pop!(backend.current_slots)
        decl
    end
    name, rettype
end

function transpile(backend::GL, block::Expr)
    if block.head == :block || block.head == :body
        transpile(backend, block.args)
    else
        # I knoow, Val no good here... But it makes the function overwritable and composable, which might be nice
        # in order to have unspecialized fall backs which can be specialized
        #println("will transpile: $(block.head) ", block)
        return transpile(backend, block, Val{block.head}())
    end
end

function is_if(block::GTBlock)
    (
        isa(block.entry, Expr) &&
        block.entry.head == :gotoifnot
    )
end
function is_elseif(block::GTBlock)
    (
        isa(block.entry, Tuple) &&
        isa(first(block.entry), LabelNode) &&
        isa(last(block.entry), Expr) &&
        last(block.entry).head == :gotoifnot
    )
end
function is_else(block::GTBlock)
    isa(block.entry, LabelNode) && isnull(block.out)
end

# (╯°□°）╯︵ ┻━┻
function deal_with_goto(backend, elem, state, list, block_source)
    labels = Set(Int[])
    blocks = GTBlock[]
    while true
        if isa(elem, Expr) && elem.head == :gotoifnot
            condition, label = elem.args
            push!(labels, label)
            push!(blocks, GTBlock(elem))
        elseif (isa(elem, LabelNode) && elem.label in labels)
            setdiff!(labels, elem.label)
            isempty(labels) && break # found last label
            elem0, state0 = elem, state
            elem, state = next(list, state)
            while isa(elem, LineNumberNode)
                elem, state = next(list, state)
            end
            if !(isa(elem, Expr) && elem.head == :gotoifnot)
                elem, state = elem0, state0 # reset peek
                push!(blocks, GTBlock(elem))
            else
                push!(labels, elem.args[2])
                push!(blocks, GTBlock((elem0, elem)))
            end
        elseif isa(elem, GotoNode)
            last(blocks).out = Nullable(elem) #close block
            push!(labels, elem.label)
        elseif isa(elem, Expr) && elem.head == :gotoifnot
            condition, label = elem.args
            push!(labels, label)
        else
            push!(last(blocks).block, elem) # insert statements of block
        end
        done(list, state) && break
        elem, state = next(list, state)
    end
    if done(list, state) && !isempty(labels)
        error("Oh my... Couldn't find all goto labels")
    end
    indents = backend.indentation_level
    indspace = repeat("    ", indents)
    backend.indentation_level += 1
    for block in blocks
        bodysrc = source(transpile(backend, block.block))
        if is_if(block)
            cond = block.entry.args[1]
            condsrc = source(transpile(backend, cond))
            block_source *= """
$(indspace)if($condsrc){
$bodysrc
$(indspace)}
"""
        elseif is_elseif(block)
            cond = block.entry[2].args[1]
            condsrc = source(transpile(backend, cond))
            block_source *= """
$(indspace)else if($condsrc){
$bodysrc
$(indspace)}
"""
        elseif is_else(block)
            block_source *= """
$(indspace)else{
$bodysrc
$(indspace)}
"""
        else
            error("Block $block not supported yet")
        end
    end
    backend.indentation_level = indents
    elem, state = next(list, state) # move one after last labelnode
    elem, state, block_source
end

function transpile(backend::GL, list::AbstractVector)
    last_type = Any
    block_source = ""
    state = start(list)
    while !done(list, state)
        elem, state = next(list, state)
        if isa(elem, Expr) && (elem.head == :gotoifnot || isa(elem, GotoNode))
            elem, state, block_source = deal_with_goto(backend, elem, state, list, block_source)
        end
        line = transpile(backend, elem)
        line == nothing && continue
        block_source *= declare_block(backend, source(line))
        last_type = returntype(line)
    end
    return TExpr(block_source, last_type)
end

transpile(backend::GL, expr::Void) = nothing

function transpile(backend::GL, expr::LineNumberNode)
    TExpr("//julia source line: $(expr.line)", Void)
end
"""
concrete values
"""
function transpile(backend::GL, value)
    cnv_value = gl_convert(value)
    TExpr(string(cnv_value), typeof(cnv_value))
end

function transpile(backend::GL, expr::Slot)
    src_t = last(backend.current_slots)[expr]
    TExpr(src_t...)
end
function transpile(backend::GL, expr::Expr, ::Val{:(=)})
    lh, rh = map(x->transpile(backend, x), expr.args)
    TExpr("$(source(lh)) = $(source(rh))", Void)
end

function transpile(backend::GL, expr::Expr, ::Val{:call})
    fun = shift!(expr.args)

    if isbox(fun) # ignore boxes, LOL \( ﾟ◡ﾟ)/
        @assert length(expr.args) == 2
        ret_type, inner_expr = expr.args
        t = transpile(backend, inner_expr)
        @assert returntype(t) == ret_type
        return t
    end
    if fun == GlobalRef(Core, :getfield)
        @assert length(expr.args) == 2
        expr, field = expr.args
        expr_trp = transpile(backend, expr)
        field_s = extract_sym(field)
        typ = returntype(expr_trp)
        return TExpr("$(source(expr_trp)).$(field_s)", fieldtype(typ, field_s))
    end
    args = map(x->transpile(backend, x), expr.args)

    fun_name, return_type = transpile(backend, fun, returntype.(args))
    transpilat = source.(args)
    call_expr = if Base.isoperator(Symbol(fun_name)) # most Julia operators should also be operators in GLSL
        if length(transpilat) == 1
            "$(fun_name)$(transpilat[1])"
        else
            foldl(transpilat) do v0, v1
                "$v0 $fun_name $v1"
            end
        end
    else
        "$fun_name($(join(transpilat, ", ")))"
    end
    TExpr(call_expr, return_type)
end

function transpile(backend::GL, expr::Expr, ::Val{:return})
    src = transpile(backend, expr.args[1])
    TExpr("return $(source(src))", returntype(src))
end

# Test

function blinnphong4{NV, T}(light, L::Vec{NV, T}, N, V, color, shading)
    diff_coeff = max(dot(L,N), T(0.0))
    # specular coefficient
    H = normalize(L+V)

    spec_coeff = max(dot(H,N), 0.0)^(shading.shininess)
    if diff_coeff <= 0.0
        spec_coeff = 0.0
    elseif diff_coeff <= 0.2
        # some nonesense to test elseif
        spec_coeff *= 2.0;
        spec_coeff += 1.0;
    else
        spec_coeff = spec_coeff;
        return L
    end
    # final lighting model
    return Vec3f0(
        light.ambient .* shading.ambient +
        light.diffuse .* light.diffuse_power .* color * diff_coeff +
        light.specular .* light.specular_power .* shading.specular * spec_coeff
    )
end

l = Light(Vec3f0(0), Vec3f0(0), Vec3f0(0), 1f0, Vec3f0(0), 1f0)
s = Shading(Vec3f0(0), Vec3f0(0), 1f0)
blinnphong3(l, Vec3f0(0), Vec3f0(0), Vec3f0(0), Vec3f0(0), s)
fe = FuncExpr(blinnphong3, (Light{Float32},Vec3f0, Vec3f0, Vec3f0, Vec3f0,Shading{Float32}))
fe.slots
transpile(GLBackend, blinnphong4, (Light{Float32},Vec3f0, Vec3f0, Vec3f0, Vec3f0,Shading{Float32}))
#delete!(GLBackend.function_map, ("blinnphong3", [Light{Float32},Vec3f0, Vec3f0, Vec3f0, Vec3f0,Shading{Float32}]))