struct DeviceEnergyBalanceConstraintSpec
    constraint_name::Symbol
    energy_variable::Symbol
    initial_condition::Type{<:InitialConditionType}
    pin_variable_names::Vector{Symbol}
    pout_variable_names::Vector{Symbol}
    parameter_name::Union{Nothing, String}
    forecast_label::Union{Nothing, String}
    multiplier_func::Union{Nothing, Function}
    constraint_func::Function
end

function DeviceEnergyBalanceConstraintSpec(;
    constraint_name::Symbol,
    energy_variable::Symbol,
    initial_condition::Type{<:InitialConditionType},
    constraint_func::Function,
    pin_variable_names::Vector{Symbol} = Vector{Symbol}(),
    pout_variable_names::Vector{Symbol} = Vector{Symbol}(),
    parameter_name::Union{Nothing, String} = nothing,
    forecast_label::Union{Nothing, String} = nothing,
    multiplier_func::Union{Nothing, Function} = nothing,
)
    return DeviceEnergyBalanceConstraintSpec(
        constraint_name,
        energy_variable,
        initial_condition,
        pin_variable_names,
        pout_variable_names,
        parameter_name,
        forecast_label,
        multiplier_func,
        constraint_func,
    )
end

function add_constraints!(
    optimization_container::OptimizationContainer,
    ::Type{T},
    ::Type{U},
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
    ::Type{X},
    feedforward::Union{Nothing, AbstractAffectFeedForward},
) where {
    T <: EnergyBalanceConstraint,
    U <: VariableType,
    V <: PSY.Device,
    W <: AbstractDeviceFormulation,
    X <: PM.AbstractPowerModel,
}
    use_parameters = model_has_parameters(optimization_container)
    use_forecasts = model_uses_forecasts(optimization_container)
    # @assert !(use_parameters && !use_forecasts)
    spec = DeviceEnergyBalanceConstraintSpec(
        T,
        U,
        V,
        W,
        X,
        feedforward,
        use_parameters,
        use_forecasts,
    )
    energy_balance_constraints!(optimization_container, devices, model, feedforward, spec)
end

function energy_balance_constraints!(
    optimization_container::OptimizationContainer,
    devices::IS.FlattenIteratorWrapper{T},
    model::DeviceModel{T, U},
    feedforward::Union{Nothing, AbstractAffectFeedForward},
    spec::DeviceEnergyBalanceConstraintSpec,
) where {T <: PSY.Device, U <: AbstractDeviceFormulation}
    initial_conditions =
        get_initial_conditions(optimization_container, spec.initial_condition, T)
    _apply_energy_balance_constraint_spec!(
        optimization_container,
        spec,
        devices,
        initial_conditions,
        model,
        feedforward,
    )
end

struct DeviceEnergyBalanceConstraintSpecInternal
    constraint_infos::Vector{<:EnergyBalanceConstraintInfo}
    constraint_name::Symbol
    energy_variable::Symbol
    pin_variable_names::Vector{Symbol}
    pout_variable_names::Vector{Symbol}
    param_reference::Union{Nothing, UpdateRef}
end

function _apply_energy_balance_constraint_spec!(
    optimization_container,
    spec,
    devices::IS.FlattenIteratorWrapper{T},
    initial_conditions::Vector{InitialCondition},
    model,
    ff_affected_variables,
) where {T <: PSY.Device}
    constraint_infos = Vector{EnergyBalanceConstraintInfo}(undef, length(devices))
    for (ix, ic) in enumerate(initial_conditions)
        device = ic.device
        dev_name = PSY.get_name(device)
        if !isnothing(spec.forecast_label)
            ts_vector = get_time_series(optimization_container, device, spec.forecast_label)
            multiplier = spec.multiplier_func(device)
            constraint_info = EnergyBalanceConstraintInfo(
                dev_name,
                get_efficiency(device, spec.initial_condition),
                ic,
                multiplier,
                ts_vector,
            )
        else
            constraint_info = EnergyBalanceConstraintInfo(;
                component_name = dev_name,
                efficiency_data = get_efficiency(device, spec.initial_condition),
                ic_energy = ic,
            )
        end
        constraint_infos[ix] = constraint_info
    end

    spec.constraint_func(
        optimization_container,
        DeviceEnergyBalanceConstraintSpecInternal(
            constraint_infos,
            spec.constraint_name,
            spec.energy_variable,
            spec.pin_variable_names,
            spec.pout_variable_names,
            spec.parameter_name === nothing ? nothing :
            UpdateRef{T}(spec.parameter_name, spec.forecast_label),
        ),
    )
    return
end

@doc raw"""
Constructs multi-timestep constraint from initial condition, efficiency data, and variable tuple
# Constraints
If t = 1:
``` varenergy[name, 1] == initial_conditions[ix].value + varin[name, 1]*eff_in*fraction_of_hour - varout[name, 1]*fraction_of_hour/eff_out ```
If t > 1:
``` varenergy[name, t] == varenergy[name, t-1] + varin[name, t]*eff_in*fraction_of_hour - varout[name, t]*fraction_of_hour/eff_out ```
# LaTeX
`` x^{energy}_1 == x^{energy}_{init} + frhr \eta^{in} x^{in}_1 - \frac{frhr}{\eta^{out}} x^{out}_1, \text{ for } t = 1 ``
`` x^{energy}_t == x^{energy}_{t-1} + frhr \eta^{in} x^{in}_t - \frac{frhr}{\eta^{out}} x^{out}_t, \forall t \geq 2 ``
# Arguments
* optimization_container::OptimizationContainer : the optimization_container model built in PowerSimulations
* inputs::Vector{DeviceEnergyBalanceConstraintSpecInternal} : stores constraint information
"""
function energy_balance!(
    optimization_container::OptimizationContainer,
    inputs::DeviceEnergyBalanceConstraintSpecInternal,
)
    time_steps = model_time_steps(optimization_container)
    resolution = model_resolution(optimization_container)
    fraction_of_hour = Dates.value(Dates.Minute(resolution)) / MINUTES_IN_HOUR
    names = [get_component_name(x) for x in inputs.constraint_infos]

    varenergy = get_variable(optimization_container, inputs.energy_variable)
    varin = [get_variable(optimization_container, var) for var in inputs.pin_variable_names]
    varout =
        [get_variable(optimization_container, var) for var in inputs.pout_variable_names]

    constraint = add_cons_container!(
        optimization_container,
        inputs.constraint_name,
        names,
        time_steps,
    )

    for info in inputs.constraint_infos
        eff_in = info.efficiency_data.in
        eff_out = info.efficiency_data.out
        name = get_component_name(info)
        # Create the PGAE outside of the constraint definition
        !isnothing(info.timeseries) ? ts_value = info.timeseries[1] * info.multiplier :
        ts_value = 0.0
        expr = JuMP.AffExpr(0.0)
        for var in varin
            JuMP.add_to_expression!(expr, var[name, 1], eff_in * fraction_of_hour)
        end
        for var in varout
            JuMP.add_to_expression!(expr, var[name, 1], -1.0 * fraction_of_hour / eff_out)
        end
        constraint[name, 1] = JuMP.@constraint(
            optimization_container.JuMPmodel,
            varenergy[name, 1] == info.ic_energy.value + expr + ts_value
        )
    end

    for t in time_steps[2:end], info in inputs.constraint_infos
        eff_in = info.efficiency_data.in
        eff_out = info.efficiency_data.out
        name = get_component_name(info)
        !isnothing(info.timeseries) ? ts_value = info.timeseries[t] * info.multiplier :
        ts_value = 0.0
        expr = JuMP.AffExpr(0.0, varenergy[name, t - 1] => 1.0)
        for var in varin
            JuMP.add_to_expression!(expr, var[name, t], eff_in * fraction_of_hour)
        end
        for var in varout
            JuMP.add_to_expression!(expr, var[name, t], -1.0 * fraction_of_hour / eff_out)
        end
        constraint[name, t] = JuMP.@constraint(
            optimization_container.JuMPmodel,
            varenergy[name, t] == expr + ts_value
        )
    end

    return
end

@doc raw"""
Constructs multi-timestep constraint from initial condition, efficiency data, and variable tuple
# Constraints
If t = 1:
``` varenergy[name, 1] == initial_conditions[ix].value + varin[name, 1]*eff_in*fraction_of_hour - varout[name, 1]*fraction_of_hour/eff_out ```
If t > 1:
``` varenergy[name, t] == varenergy[name, t-1] + varin[name, t]*eff_in*fraction_of_hour - varout[name, t]*fraction_of_hour/eff_out ```
# LaTeX
`` x^{energy}_1 == x^{energy}_{init} + frhr \eta^{in} x^{in}_1 - \frac{frhr}{\eta^{out}} x^{out}_1, \text{ for } t = 1 ``
`` x^{energy}_t == x^{energy}_{t-1} + frhr \eta^{in} x^{in}_t - \frac{frhr}{\eta^{out}} x^{out}_t, \forall t \geq 2 ``
# Arguments
* optimization_container::OptimizationContainer : the optimization_container model built in PowerSimulations
* inputs::Vector{DeviceEnergyBalanceConstraintSpecInternal} : stores constraint information
"""
function energy_balance_param!(
    optimization_container::OptimizationContainer,
    inputs::DeviceEnergyBalanceConstraintSpecInternal,
)
    time_steps = model_time_steps(optimization_container)
    resolution = model_resolution(optimization_container)
    fraction_of_hour = Dates.value(Dates.Minute(resolution)) / MINUTES_IN_HOUR
    names = [get_component_name(x) for x in inputs.constraint_infos]
    has_parameter_data = !isnothing(inputs.param_reference)

    varenergy = get_variable(optimization_container, inputs.energy_variable)
    varin = [get_variable(optimization_container, var) for var in inputs.pin_variable_names]
    varout =
        [get_variable(optimization_container, var) for var in inputs.pout_variable_names]

    if has_parameter_data
        container = add_param_container!(
            optimization_container,
            inputs.param_reference,
            names,
            time_steps,
        )
        multiplier = get_multiplier_array(container)
        param = get_parameter_array(container)
    end

    constraint = add_cons_container!(
        optimization_container,
        inputs.constraint_name,
        names,
        time_steps,
    )

    for info in inputs.constraint_infos
        eff_in = info.efficiency_data.in
        eff_out = info.efficiency_data.out
        name = get_component_name(info)
        # Create the PGAE outside of the constraint definition
        expr = zero(PGAE)
        for var in varin
            JuMP.add_to_expression!(expr, var[name, 1], eff_in * fraction_of_hour)
        end
        for var in varout
            JuMP.add_to_expression!(expr, var[name, 1], -1 * fraction_of_hour / eff_out)
        end
        if has_parameter_data
            multiplier[name, 1] = info.multiplier
            param[name, 1] =
                PJ.add_parameter(optimization_container.JuMPmodel, info.timeseries[1])
            JuMP.add_to_expression!(expr, param[name, 1], multiplier[name, 1])
        end
        constraint[name, 1] = JuMP.@constraint(
            optimization_container.JuMPmodel,
            varenergy[name, 1] == info.ic_energy.value + expr
        )
    end

    for t in time_steps[2:end], info in inputs.constraint_infos
        eff_in = info.efficiency_data.in
        eff_out = info.efficiency_data.out
        name = get_component_name(info)
        expr = expr = zero(PGAE)
        JuMP.add_to_expression!(expr, varenergy[name, t - 1])
        for var in varin
            JuMP.add_to_expression!(expr, var[name, t], eff_in * fraction_of_hour)
        end
        for var in varout
            JuMP.add_to_expression!(expr, var[name, t], -1 * fraction_of_hour / eff_out)
        end
        if has_parameter_data
            multiplier[name, t] = info.multiplier
            param[name, t] =
                PJ.add_parameter(optimization_container.JuMPmodel, info.timeseries[t])
            JuMP.add_to_expression!(expr, param[name, t], multiplier[name, t])
        end
        constraint[name, t] =
            JuMP.@constraint(optimization_container.JuMPmodel, varenergy[name, t] == expr)
    end

    return
end

########## Old constraint function that will be deleted after storage refactor is complete

@doc raw"""
Constructs multi-timestep constraint from initial condition, efficiency data, and variable tuple

# Constraints

If t = 1:

``` varenergy[name, 1] == initial_conditions[ix].value + varin[name, 1]*eff_in*fraction_of_hour - varout[name, 1]*fraction_of_hour/eff_out ```

If t > 1:

``` varenergy[name, t] == varenergy[name, t-1] + varin[name, t]*eff_in*fraction_of_hour - varout[name, t]*fraction_of_hour/eff_out ```

# LaTeX

`` x^{energy}_1 == x^{energy}_{init} + frhr \eta^{in} x^{in}_1 - \frac{frhr}{\eta^{out}} x^{out}_1, \text{ for } t = 1 ``

`` x^{energy}_t == x^{energy}_{t-1} + frhr \eta^{in} x^{in}_t - \frac{frhr}{\eta^{out}} x^{out}_t, \forall t \geq 2 ``

# Arguments
* optimization_container::OptimizationContainer : the optimization_container model built in PowerSimulations
* initial_conditions::Vector{InitialCondition} : for time zero 'varenergy'
* efficiency_data::Tuple{Vector{String}, Vector{InOut}} :: charging/discharging efficiencies
* cons_name::Symbol : name of the constraint
* var_names::Tuple{Symbol, Symbol, Symbol} : the names of the variables
- : var_names[1] : varin
- : var_names[2] : varout
- : var_names[3] : varenergy

"""
function energy_balance(
    optimization_container::OptimizationContainer,
    initial_conditions::Vector{InitialCondition},
    efficiency_data::Tuple{Vector{String}, Vector{InOut}},
    cons_name::Symbol,
    var_names::Tuple{Symbol, Symbol, Symbol},
)
    parameters = model_has_parameters(optimization_container)
    time_steps = model_time_steps(optimization_container)
    resolution = model_resolution(optimization_container)
    fraction_of_hour = Dates.value(Dates.Minute(resolution)) / MINUTES_IN_HOUR
    name_index = efficiency_data[1]

    varin = get_variable(optimization_container, var_names[1])
    varout = get_variable(optimization_container, var_names[2])
    varenergy = get_variable(optimization_container, var_names[3])

    constraint =
        add_cons_container!(optimization_container, cons_name, name_index, time_steps)

    for (ix, name) in enumerate(name_index)
        eff_in = efficiency_data[2][ix].in
        eff_out = efficiency_data[2][ix].out
        # Create the PGAE outside of the constraint definition
        balance =
            initial_conditions[ix].value + varin[name, 1] * eff_in * fraction_of_hour -
            (varout[name, 1]) * fraction_of_hour / eff_out
        constraint[name, 1] = JuMP.@constraint(
            optimization_container.JuMPmodel,
            varenergy[name, 1] == balance
        )
    end

    for t in time_steps[2:end], (ix, name) in enumerate(name_index)
        eff_in = efficiency_data[2][ix].in
        eff_out = efficiency_data[2][ix].out

        constraint[name, t] = JuMP.@constraint(
            optimization_container.JuMPmodel,
            varenergy[name, t] ==
            varenergy[name, t - 1] + varin[name, t] * eff_in * fraction_of_hour -
            (varout[name, t]) * fraction_of_hour / eff_out
        )
    end

    return
end

@doc raw"""
Constructs multi-timestep constraint from initial condition, efficiency data, and variable tuple
# Constraints
If t = 1:
`` varenergy[name, 1] == initial_conditions[ix].value + (paraminflow[name, t] - varspill[name, 1] - varout[name, 1])*fraction_of_hour ``
If t > 1:
`` varenergy[name, t] == varenergy[name, t-1] + (paraminflow[name, t] - varspill[name, t] - varout[name, t])*fraction_of_hour ``
`` varenergy[name, end] >= paramenergytarget[name, end]
# LaTeX
`` x^{energy}_1 == x^{energy}_{init} + frhr  (x^{in}_1 - x^{spillage}_1 -  x^{out}_1), \text{ for } t = 1 ``
`` x^{energy}_t == x^{energy}_{t-1} + frhr (x^{in}_t - x^{spillage}_t - x^{out}_t), \forall t \geq 2 ``
`` x^{energy}_t >= x^{energy}_{target} \text{ for } t = end ``
# Arguments
* optimization_container::OptimizationContainer : the optimization_container model built in PowerSimulations
* initial_conditions::Vector{InitialCondition} : for time zero 'varenergy'
* time_series_data::Tuple{Vector{DeviceTimeSeriesConstraintInfo}, Vector{DeviceTimeSeriesConstraintInfo}} : forecast information
- : time_series_data[1] : Inflow energy forecast information
- : time_series_data[2] : Target reservoir storage forecast information
* cons_names::Tuple{Symbol, Symbol} : name of the constraints
- : cons_names[1] : energy balance constraint name
- : cons_names[2] : energy target constraint name
* var_names::Tuple{Symbol, Symbol, Symbol} : the names of the variables
- : var_names[1] : varspill
- : var_names[2] : varout
- : var_names[3] : varenergy
* param_reference::UpdateRef : UpdateRef to access the inflow parameter
"""
function energy_balance_hydro_param!(
    optimization_container::OptimizationContainer,
    initial_conditions::Vector{InitialCondition},
    time_series_data::Tuple{
        Vector{DeviceTimeSeriesConstraintInfo},
        Vector{DeviceTimeSeriesConstraintInfo},
    },
    cons_names::Tuple{Symbol, Symbol},
    var_names::Tuple{Symbol, Symbol, Symbol},
    param_references::Tuple{UpdateRef, UpdateRef},
)
    time_steps = model_time_steps(optimization_container)
    resolution = model_resolution(optimization_container)
    fraction_of_hour = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR

    inflow_data = time_series_data[1]
    target_data = time_series_data[2]

    name_index = [get_component_name(d) for d in inflow_data]

    varspill = get_variable(optimization_container, var_names[1])
    varout = get_variable(optimization_container, var_names[2])
    varenergy = get_variable(optimization_container, var_names[3])

    balance_cons_name = cons_names[1]
    target_cons_name = cons_names[2]
    balance_param_reference = param_references[1]
    target_param_reference = param_references[2]

    container_inflow = add_param_container!(
        optimization_container,
        balance_param_reference,
        name_index,
        time_steps,
    )
    param_inflow = get_parameter_array(container_inflow)
    multiplier_inflow = get_multiplier_array(container_inflow)
    container_target = add_param_container!(
        optimization_container,
        target_param_reference,
        name_index,
        time_steps,
    )
    param_target = get_parameter_array(container_target)
    multiplier_target = get_multiplier_array(container_target)

    balance_constraint = add_cons_container!(
        optimization_container,
        balance_cons_name,
        name_index,
        time_steps,
    )
    target_constraint =
        add_cons_container!(optimization_container, target_cons_name, name_index, 1)

    for (ix, d) in enumerate(inflow_data)
        name = get_component_name(d)
        multiplier_inflow[name, 1] = d.multiplier
        param_inflow[name, 1] =
            add_parameter(optimization_container.JuMPmodel, d.timeseries[1])
        exp =
            initial_conditions[ix].value +
            (
                multiplier_inflow[name, 1] * param_inflow[name, 1] - varspill[name, 1] -
                varout[name, 1]
            ) * fraction_of_hour
        balance_constraint[name, 1] =
            JuMP.@constraint(optimization_container.JuMPmodel, varenergy[name, 1] == exp)

        for t in time_steps[2:end]
            multiplier_inflow[name, t] = d.multiplier
            param_inflow[name, t] =
                add_parameter(optimization_container.JuMPmodel, d.timeseries[t])
            exp =
                varenergy[name, t - 1] +
                (
                    multiplier_inflow[name, t] * param_inflow[name, t] - varspill[name, t] -
                    varout[name, t]
                ) * fraction_of_hour
            balance_constraint[name, t] = JuMP.@constraint(
                optimization_container.JuMPmodel,
                varenergy[name, t] == exp
            )
        end
    end

    for (ix, d) in enumerate(target_data)
        name = get_component_name(d)
        for t in time_steps
            param_target[name, t] =
                add_parameter(optimization_container.JuMPmodel, d.timeseries[t])
            multiplier_target[name, t] = d.multiplier
        end
        target_constraint[name, 1] = JuMP.@constraint(
            optimization_container.JuMPmodel,
            varenergy[name, time_steps[end]] >=
            d.multiplier * param_target[name, time_steps[end]]
        )
    end

    return
end

@doc raw"""
Constructs multi-timestep constraint from initial condition, efficiency data, and variable tuple
# Constraints
If t = 1:
`` varenergy[name, 1] == initial_conditions[ix].value + (paraminflow[name, t] - varspill[name, 1] - varout[name, 1])*fraction_of_hour ``
If t > 1:
`` varenergy[name, t] == varenergy[name, t-1] + (paraminflow[name, t] - varspill[name, t] - varout[name, t])*fraction_of_hour ``
`` varenergy[name, end] >= paramenergytarget[name, end]
# LaTeX
`` x^{energy}_1 == x^{energy}_{init} + frhr  (x^{in}_1 - x^{spillage}_1 -  x^{out}_1), \text{ for } t = 1 ``
`` x^{energy}_t == x^{energy}_{t-1} + frhr (x^{in}_t - x^{spillage}_t - x^{out}_t), \forall t \geq 2 ``
`` x^{energy}_t >= x^{energy}_{target} \text{ for } t = end ``
# Arguments
* optimization_container::OptimizationContainer : the optimization_container model built in PowerSimulations
* initial_conditions::Vector{InitialCondition} : for time zero 'varenergy'
* time_series_data::Tuple{Vector{DeviceTimeSeriesConstraintInfo}, Vector{DeviceTimeSeriesConstraintInfo}} : forecast information
- : time_series_data[1] : Inflow energy forecast information
- : time_series_data[2] : Target reservoir storage forecast information
* cons_names::Tuple{Symbol, Symbol} : name of the constraints
- : cons_names[1] : energy balance constraint name
- : cons_names[2] : energy target constraint name
* var_names::Tuple{Symbol, Symbol, Symbol} : the names of the variables
- : var_names[1] : varspill
- : var_names[2] : varout
- : var_names[3] : varenergy
"""
function energy_balance_hydro!(
    optimization_container::OptimizationContainer,
    initial_conditions::Vector{InitialCondition},
    time_series_data::Tuple{
        Vector{DeviceTimeSeriesConstraintInfo},
        Vector{DeviceTimeSeriesConstraintInfo},
    },
    cons_names::Tuple{Symbol, Symbol},
    var_names::Tuple{Symbol, Symbol, Symbol},
)
    time_steps = model_time_steps(optimization_container)
    resolution = model_resolution(optimization_container)
    fraction_of_hour = Dates.value(Dates.Minute(resolution)) / MINUTES_IN_HOUR

    inflow_data = time_series_data[1]
    target_data = time_series_data[2]

    name_index = [get_component_name(d) for d in inflow_data]

    varspill = get_variable(optimization_container, var_names[1])
    varout = get_variable(optimization_container, var_names[2])
    varenergy = get_variable(optimization_container, var_names[3])

    balance_cons_name = cons_names[1]
    target_cons_name = cons_names[2]

    balance_constraint = add_cons_container!(
        optimization_container,
        balance_cons_name,
        name_index,
        time_steps,
    )
    target_constraint =
        add_cons_container!(optimization_container, target_cons_name, name_index, 1)

    for (ix, d) in enumerate(inflow_data)
        name = get_component_name(d)
        balance_constraint[name, 1] = JuMP.@constraint(
            optimization_container.JuMPmodel,
            varenergy[name, 1] ==
            initial_conditions[ix].value +
            (d.multiplier * d.timeseries[1] - varspill[name, 1] - varout[name, 1]) *
            fraction_of_hour
        )

        for t in time_steps[2:end]
            balance_constraint[name, t] = JuMP.@constraint(
                optimization_container.JuMPmodel,
                varenergy[name, t] ==
                varenergy[name, t - 1] +
                (d.multiplier * d.timeseries[t] - varspill[name, t] - varout[name, t]) *
                fraction_of_hour
            )
        end
    end

    for (ix, d) in enumerate(target_data)
        name = get_component_name(d)
        target_constraint[name, 1] = JuMP.@constraint(
            optimization_container.JuMPmodel,
            varenergy[name, time_steps[end]] >=
            d.multiplier * d.timeseries[time_steps[end]]
        )
    end

    return
end

@doc raw"""
Constructs multi-timestep constraint from initial condition, efficiency data, and variable tuple for pumped hydro
# Constraints
If t = 1:
``` varenergy_up[name, 1] == initial_conditions[ix].value + (param_inflow[name, t] + varin[name, 1] - varspill[name, 1] - varout[name, 1])*fraction_of_hour ```
If t > 1:
``` varenergy_up[name, t] == varenergy_up[name, t-1] + (param_inflow[name, t] + varin[name, t] - varspill[name, t] - varout[name, t])*fraction_of_hour ```
# LaTeX
`` x^{energy}_1 == x^{energy}_{init} + frhr  (x^{in}_1 + x^{in}_1 - x^{spillage}_1 -  x^{out}_1), \text{ for } t = 1 ``
`` x^{energy}_t == x^{energy}_{t-1} + frhr (x^{in}_t + x^{in}_t - x^{spillage}_t - x^{out}_t), \forall t \geq 2 ``
# Arguments
* optimization_container::OptimizationContainer : the optimization_container model built in PowerSimulations
* initial_conditions::Vector{InitialCondition} : for time zero 'varenergy_up'
* inflow_data::Vector{DeviceTimeSeriesConstraintInfo} :: Inflow energy forecast information
* cons_name::Symbol : name of the constraint
* var_names::Tuple{Symbol, Symbol, Symbol, Symbol} : the names of the variables
- : var_names[1] : varspill
- : var_names[2] : varout
- : var_names[3] : varenergy_up
- : var_names[4] : varin
* param_reference::UpdateRef : UpdateRef to access the inflow parameter
"""
function energy_balance_hydro_param!(
    optimization_container::OptimizationContainer,
    initial_conditions::Vector{InitialCondition},
    ts_data::Tuple{
        Vector{DeviceTimeSeriesConstraintInfo},
        Vector{DeviceTimeSeriesConstraintInfo},
    },
    cons_name::Tuple{Symbol, Symbol},
    var_names::Tuple{Symbol, Symbol, Symbol, Symbol, Symbol},
    param_reference::Tuple{UpdateRef, UpdateRef},
)
    time_steps = model_time_steps(optimization_container)
    resolution = model_resolution(optimization_container)
    fraction_of_hour = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR
    inflow_data = ts_data[1]
    outflow_data = ts_data[2]
    inflow_name_index = [get_component_name(d) for d in inflow_data]
    outflow_name_index = [get_component_name(d) for d in outflow_data]

    varspill = get_variable(optimization_container, var_names[1])
    varout = get_variable(optimization_container, var_names[2])
    varenergy_up = get_variable(optimization_container, var_names[3])
    varin = get_variable(optimization_container, var_names[4])
    varenergy_down = get_variable(optimization_container, var_names[5])

    container_inflow = add_param_container!(
        optimization_container,
        param_reference[1],
        inflow_name_index,
        time_steps,
    )
    param_inflow = get_parameter_array(container_inflow)
    multiplier_inflow = get_multiplier_array(container_inflow)
    container_outflow = add_param_container!(
        optimization_container,
        param_reference[2],
        outflow_name_index,
        time_steps,
    )
    param_outflow = get_parameter_array(container_outflow)
    multiplier_outflow = get_multiplier_array(container_outflow)
    constraint_up = add_cons_container!(
        optimization_container,
        cons_name[1],
        inflow_name_index,
        time_steps,
    )
    constraint_down = add_cons_container!(
        optimization_container,
        cons_name[2],
        outflow_name_index,
        time_steps,
    )

    for (ix, d) in enumerate(inflow_data)
        name = get_component_name(d)
        pump_eff = 1.0 # TODO: get pump efficiency PSY.get_pump_efficiency(d)
        multiplier_inflow[name, 1] = d.multiplier
        param_inflow[name, 1] =
            add_parameter(optimization_container.JuMPmodel, d.timeseries[1])
        exp =
            initial_conditions[ix].value +
            (
                multiplier_inflow[name, 1] * param_inflow[name, 1] +
                varin[name, 1] * pump_eff - varspill[name, 1] - varout[name, 1]
            ) * fraction_of_hour
        constraint_up[name, 1] =
            JuMP.@constraint(optimization_container.JuMPmodel, varenergy_up[name, 1] == exp)

        for t in time_steps[2:end]
            multiplier_inflow[name, t] = d.multiplier
            param_inflow[name, t] =
                add_parameter(optimization_container.JuMPmodel, d.timeseries[t])
            exp =
                varenergy_up[name, t - 1] +
                (
                    multiplier_inflow[name, t] * param_inflow[name, t] +
                    varin[name, t] * pump_eff - varspill[name, t] - varout[name, t]
                ) * fraction_of_hour
            constraint_up[name, t] = JuMP.@constraint(
                optimization_container.JuMPmodel,
                varenergy_up[name, t] == exp
            )
        end
    end

    for (ix, d) in enumerate(outflow_data)
        name = get_component_name(d)
        pump_eff = 1.0 # TODO: get pump efficiency PSY.get_pump_efficiency(d)
        multiplier_outflow[name, 1] = d.multiplier
        param_outflow[name, 1] =
            add_parameter(optimization_container.JuMPmodel, d.timeseries[1])
        exp =
            initial_conditions[ix].value +
            (
                varspill[name, 1] + varout[name, 1] -
                multiplier_outflow[name, 1] * param_outflow[name, 1] -
                varin[name, 1] * pump_eff
            ) * fraction_of_hour
        constraint_down[name, 1] = JuMP.@constraint(
            optimization_container.JuMPmodel,
            varenergy_down[name, 1] == exp
        )

        for t in time_steps[2:end]
            multiplier_outflow[name, t] = d.multiplier
            param_outflow[name, t] =
                add_parameter(optimization_container.JuMPmodel, d.timeseries[t])
            exp =
                varenergy_down[name, t - 1] +
                (
                    varspill[name, t] + varout[name, t] -
                    multiplier_outflow[name, t] * param_outflow[name, t] -
                    varin[name, t] * pump_eff
                ) * fraction_of_hour
            constraint_down[name, t] = JuMP.@constraint(
                optimization_container.JuMPmodel,
                varenergy_down[name, t] == exp
            )
        end
    end

    return
end

@doc raw"""
Constructs multi-timestep constraint from initial condition, efficiency data, and variable tuple for pumped hydro
# Constraints
If t = 1:
``` varenergy[name, 1] == initial_conditions[ix].value + (paraminflow[name, t] + varin[name, 1] - varspill[name, 1] - varout[name, 1])*fraction_of_hour ```
If t > 1:
``` varenergy[name, t] == varenergy[name, t-1] + (paraminflow[name, t] + varin[name, t] - varspill[name, t] - varout[name, t])*fraction_of_hour ```
# LaTeX
`` x^{energy}_1 == x^{energy}_{init} + frhr  (x^{in}_1 + x^{in}_1 - x^{spillage}_1 -  x^{out}_1), \text{ for } t = 1 ``
`` x^{energy}_t == x^{energy}_{t-1} + frhr (x^{in}_t + x^{in}_t - x^{spillage}_t - x^{out}_t), \forall t \geq 2 ``
# Arguments
* optimization_container::OptimizationContainer : the optimization_container model built in PowerSimulations
* initial_conditions::Vector{InitialCondition} : for time zero 'varenergy'
* inflow_data::TVector{DeviceTimeSeriesConstraintInfo} :: Inflow energy forecast information
* cons_name::Symbol : name of the constraint
* var_names::Tuple{Symbol, Symbol, Symbol, Symbol} : the names of the variables
- : var_names[1] : varspill
- : var_names[2] : varout
- : var_names[3] : varenergy
- : var_names[4] : varin
"""
function energy_balance_hydro!(
    optimization_container::OptimizationContainer,
    initial_conditions::Vector{InitialCondition},
    ts_data::Tuple{
        Vector{DeviceTimeSeriesConstraintInfo},
        Vector{DeviceTimeSeriesConstraintInfo},
    },
    cons_name::Tuple{Symbol, Symbol},
    var_names::Tuple{Symbol, Symbol, Symbol, Symbol, Symbol},
)
    time_steps = model_time_steps(optimization_container)
    resolution = model_resolution(optimization_container)
    fraction_of_hour = Dates.value(Dates.Minute(resolution)) / MINUTES_IN_HOUR
    inflow_data = ts_data[1]
    outflow_data = ts_data[2]
    inflow_name_index = [get_component_name(d) for d in inflow_data]
    outflow_name_index = [get_component_name(d) for d in outflow_data]

    varspill = get_variable(optimization_container, var_names[1])
    varout = get_variable(optimization_container, var_names[2])
    varenergy_up = get_variable(optimization_container, var_names[3])
    varin = get_variable(optimization_container, var_names[4])
    varenergy_down = get_variable(optimization_container, var_names[5])

    constraint_up = add_cons_container!(
        optimization_container,
        cons_name[1],
        inflow_name_index,
        time_steps,
    )
    constraint_down = add_cons_container!(
        optimization_container,
        cons_name[2],
        outflow_name_index,
        time_steps,
    )

    for (ix, d) in enumerate(inflow_data)
        name = get_component_name(d)
        pump_eff = 1.0 # TODO: get pump efficiency PSY.get_pump_efficiency(d)
        constraint_up[name, 1] = JuMP.@constraint(
            optimization_container.JuMPmodel,
            varenergy_up[name, 1] ==
            initial_conditions[ix].value +
            (
                d.multiplier * d.timeseries[1] + varin[name, 1] * pump_eff -
                varspill[name, 1] - varout[name, 1]
            ) * fraction_of_hour
        )

        for t in time_steps[2:end]
            constraint_up[name, t] = JuMP.@constraint(
                optimization_container.JuMPmodel,
                varenergy_up[name, t] ==
                varenergy_up[name, t - 1] +
                (
                    d.multiplier * d.timeseries[t] + varin[name, t] * pump_eff -
                    varspill[name, t] - varout[name, t]
                ) * fraction_of_hour
            )
        end
    end

    for (ix, d) in enumerate(outflow_data)
        name = get_component_name(d)
        pump_eff = 1.0 # TODO: get pump efficiency PSY.get_pump_efficiency(d)
        constraint_down[name, 1] = JuMP.@constraint(
            optimization_container.JuMPmodel,
            varenergy_down[name, 1] ==
            initial_conditions[ix].value +
            (
                varspill[name, 1] + varout[name, 1] - d.multiplier * d.timeseries[1] -
                varin[name, 1] * pump_eff
            ) * fraction_of_hour
        )

        for t in time_steps[2:end]
            constraint_down[name, t] = JuMP.@constraint(
                optimization_container.JuMPmodel,
                varenergy_down[name, t] ==
                varenergy_down[name, t - 1] +
                (
                    varspill[name, t] + varout[name, t] - d.multiplier * d.timeseries[t] - varin[name, t] * pump_eff
                ) * fraction_of_hour
            )
        end
    end
    return
end


# *********************************************************************************
# REopt, Copyright (c) 2019-2020, Alliance for Sustainable Energy, LLC.
# All rights reserved.
#
# 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 the copyright holder 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 HOLDER 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.
# *********************************************************************************
"""
    Metrics

Convenience mutable struct for passing data between proforma methods
"""
mutable struct Metrics
    federal_itc::Float64
    om_series::Array{Float64, 1}
    om_series_bau::Array{Float64, 1}
    total_pbi::Array{Float64, 1}
    total_pbi_bau::Array{Float64, 1}
    total_depreciation::Array{Float64, 1}
    total_ibi_and_cbi::Float64
end

"""
    calculate_proforma_metrics(p::REoptInputs, d::Dict)

Recreates the ProForma spreadsheet calculations to get the simple payback period, irr, net present cost (3rd
party case), and payment to third party (3rd party case).

return Dict(
    "simple_payback_years" => 0.0,
    "internal_rate_of_return" => 0.0,
    "net_present_cost" => 0.0,
    "annualized_payment_to_third_party" => 0.0,
    "offtaker_annual_free_cashflows" => Float64[],
    "offtaker_annual_free_cashflows_bau" => Float64[],
    "offtaker_discounted_annual_free_cashflows" => Float64[],
    "offtaker_discounted_annual_free_cashflows_bau" => Float64[],
    "developer_annual_free_cashflows" => Float64[]
)
"""
function proforma_results(p::REoptInputs, d::Dict)
    r = Dict(
        "simple_payback_years" => 0.0,
        "internal_rate_of_return" => 0.0,
        "net_present_cost" => 0.0,
        "annualized_payment_to_third_party" => 0.0,
        "offtaker_annual_free_cashflows" => Float64[],
        "offtaker_annual_free_cashflows_bau" => Float64[],
        "offtaker_discounted_annual_free_cashflows" => Float64[],
        "offtaker_discounted_annual_free_cashflows_bau" => Float64[],
        "developer_annual_free_cashflows" => Float64[]
    )
    years = p.s.financial.analysis_years
    escalate_elec(val) = [-1 * val * (1 + p.s.financial.elec_cost_escalation_pct)^yr for yr in 1:years]
    escalate_om(val) = [val * (1 + p.s.financial.om_cost_escalation_pct)^yr for yr in 1:years]
    third_party = p.s.financial.third_party_ownership

    # Create placeholder variables to store summed totals across all relevant techs
    m = Metrics(0, zeros(years), zeros(years), zeros(years), zeros(years), zeros(years), 0)

    # calculate PV o+m costs, incentives, and depreciation
    for pv in p.s.pvs
        update_metrics(m, p, pv, pv.name, d, third_party)
    end

    # calculate Wind o+m costs, incentives, and depreciation
    if "Wind" in keys(d) && d["Wind"]["size_kw"] > 0
        update_metrics(m, p, p.s.wind, "Wind", d, third_party)
    end

    # calculate Storage o+m costs, incentives, and depreciation
    if "Storage" in keys(d) && d["Storage"]["size_kw"] > 0
        # TODO handle other types of storage
        storage = p.s.storage.raw_inputs[:elec]
        total_kw = d["Storage"]["size_kw"]
        total_kwh = d["Storage"]["size_kwh"]
        capital_cost = total_kw * storage.installed_cost_per_kw + total_kwh * storage.installed_cost_per_kwh
        battery_replacement_year = storage.battery_replacement_year
        battery_replacement_cost = -1 * ((total_kw * storage.replace_cost_per_kw) + (
                    total_kwh * storage.replace_cost_per_kwh))
        m.om_series += [yr != battery_replacement_year ? 0 : battery_replacement_cost for yr in 1:years]

        # storage only has cbi in the API
        cbi = total_kw * storage.total_rebate_per_kw + total_kwh * storage.total_rebate_per_kwh
        m.total_ibi_and_cbi += cbi

        # ITC
        federal_itc_basis = capital_cost  # bug in v1 subtracted cbi from capital_cost here
        federal_itc_amount = storage.total_itc_pct * federal_itc_basis
        m.federal_itc += federal_itc_amount

        # Depreciation
        if storage.macrs_option_years in [5, 7]
            schedule = []
            if storage.macrs_option_years == 5
                schedule = p.s.financial.macrs_five_year
            elseif storage.macrs_option_years == 7
                schedule = p.s.financial.macrs_seven_year
            end
            macrs_bonus_basis = federal_itc_basis * (1 - storage.total_itc_pct * storage.macrs_itc_reduction)
            macrs_basis = macrs_bonus_basis * (1 - storage.macrs_bonus_pct)

            depreciation_schedule = zeros(years)
            for (i, r) in enumerate(schedule)
                if i < length(depreciation_schedule)
                    depreciation_schedule[i] = macrs_basis * r
                end
            end
            depreciation_schedule[1] += storage.macrs_bonus_pct * macrs_bonus_basis
            m.total_depreciation += depreciation_schedule
        end
    end

    # calculate Generator o+m costs, incentives, and depreciation
    if "Generator" in keys(d) && d["Generator"]["size_kw"] > 0
        # In the two party case the developer does not include the fuel cost in their costs
        # It is assumed that the offtaker will pay for this at a rate that is not marked up
        # to cover developer profits
        fixed_and_var_om = d["Generator"]["year_one_fixed_om_cost"] + d["Generator"]["year_one_variable_om_cost"]
        fixed_and_var_om_bau = 0.0
        year_one_fuel_cost_bau = 0.0
        if p.s.generator.existing_kw > 0
            fixed_and_var_om_bau = d["Generator"]["year_one_fixed_om_cost_bau"] +
                                   d["Generator"]["year_one_variable_om_cost_bau"]
            year_one_fuel_cost_bau = d["Generator"]["year_one_fuel_cost_bau"]
        end
        if !third_party
            annual_om = -1 * (fixed_and_var_om + d["Generator"]["year_one_fuel_cost"])

            annual_om_bau = -1 * (fixed_and_var_om_bau + year_one_fuel_cost_bau)
        else
            annual_om = -1 * fixed_and_var_om

            annual_om_bau = -1 * fixed_and_var_om_bau
        end

        m.om_series += escalate_om(annual_om)
        m.om_series_bau += escalate_om(annual_om_bau)
    end

    # Optimal Case calculations
    electricity_bill_series = escalate_elec(d["ElectricTariff"]["year_one_bill"])
    export_credit_series = escalate_elec(d["ElectricTariff"]["year_one_export_benefit"])

    # In the two party case the electricity and export credits are incurred by the offtaker not the developer
    if third_party
        total_operating_expenses = m.om_series
        tax_pct = p.s.financial.owner_tax_pct
    else
        total_operating_expenses = electricity_bill_series + export_credit_series + m.om_series
        tax_pct = p.s.financial.offtaker_tax_pct
    end

    # Apply taxes to operating expenses
    if tax_pct > 0
        deductable_operating_expenses_series = copy(total_operating_expenses)
    else
        deductable_operating_expenses_series = zeros(years)
    end

    operating_expenses_after_tax = (total_operating_expenses - deductable_operating_expenses_series) +
                                    deductable_operating_expenses_series * (1 - tax_pct)
    total_cash_incentives = m.total_pbi * (1 - tax_pct)
    free_cashflow_without_year_zero = m.total_depreciation * tax_pct + total_cash_incentives + operating_expenses_after_tax
    free_cashflow_without_year_zero[1] += m.federal_itc
    free_cashflow = append!([(-1 * d["Financial"]["initial_capital_costs"]) + m.total_ibi_and_cbi], free_cashflow_without_year_zero)

    # At this point the logic branches based on third-party ownership or not - see comments
    if third_party  # get cumulative cashflow for developer
        r["developer_annual_free_cashflows"] = copy(free_cashflow)
        discounted_developer_cashflow = [v / ((1 + p.s.financial.owner_discount_pct)^(yr-1)) for (yr, v) in
                                         enumerate(r["developer_annual_free_cashflows"])]
        r["net_present_cost"] = sum(discounted_developer_cashflow) * -1

        if p.s.financial.owner_discount_pct != 0
            capital_recovery_factor = (p.s.financial.owner_discount_pct * (1 + p.s.financial.owner_discount_pct)^years) /
                                      ((1 + p.s.financial.owner_discount_pct)^years - 1) / (1 - tax_pct)
        else
            capital_recovery_factor = (1 / years) / (1 - tax_pct)
        end

        r["annualized_payment_to_third_party"] = r["net_present_cost"] * capital_recovery_factor
        annual_income_from_host = -1 * sum(discounted_developer_cashflow) * capital_recovery_factor * (1 - tax_pct)
        r["developer_annual_free_cashflows"][2:end] .+= annual_income_from_host
        r["internal_rate_of_return"] = irr(r["developer_annual_free_cashflows"])
        cumulative_cashflow = cumsum(r["developer_annual_free_cashflows"])
        net_free_cashflow = r["developer_annual_free_cashflows"]
        r["developer_annual_free_cashflows"] = round.(r["developer_annual_free_cashflows"], digits=2)

        electricity_bill_series = escalate_elec(d["ElectricTariff"]["year_one_bill"])
        electricity_bill_series_bau = escalate_elec(d["ElectricTariff"]["year_one_bill_bau"])

        export_credit_series = escalate_elec(-d["ElectricTariff"]["lifecycle_export_benefit"])
        export_credit_series_bau = escalate_elec(-d["ElectricTariff"]["lifecycle_export_benefit_bau"])

        annual_income_from_host_series = repeat([-1 * r["annualized_payment_to_third_party"]], years)

        if "Generator" in keys(d) && d["Generator"]["size_kw"] > 0
            existing_genertor_fuel_cost_series = escalate_om(-1 * d["Generator"]["year_one_fuel_cost_bau"])
            generator_fuel_cost_series = escalate_om(-1 * d["Generator"]["year_one_fuel_cost"])
        else
            existing_genertor_fuel_cost_series = zeros(years)
            generator_fuel_cost_series = zeros(years)
        end
        net_energy_costs = -electricity_bill_series_bau - export_credit_series_bau + electricity_bill_series +
                           export_credit_series + annual_income_from_host_series - existing_genertor_fuel_cost_series +
                           generator_fuel_cost_series

        if p.s.financial.owner_tax_pct > 0
            deductable_net_energy_costs = copy(net_energy_costs)
        else
            deductable_net_energy_costs = zeros(years)
        end

        r["offtaker_annual_free_cashflows"] = append!([0.0],
            electricity_bill_series + export_credit_series + generator_fuel_cost_series + annual_income_from_host_series
        )
        r["offtaker_annual_free_cashflows_bau"] = append!([0.0],
            electricity_bill_series_bau + export_credit_series_bau + existing_genertor_fuel_cost_series
            )

    else  # get cumulative cashflow for offtaker
        electricity_bill_series_bau = escalate_elec(d["ElectricTariff"]["year_one_bill_bau"])
        export_credit_series_bau = escalate_elec(-d["ElectricTariff"]["lifecycle_export_benefit_bau"])
        total_operating_expenses_bau = electricity_bill_series_bau + export_credit_series_bau + m.om_series_bau
        total_cash_incentives_bau = m.total_pbi_bau * (1 - p.s.financial.offtaker_tax_pct)

        if p.s.financial.offtaker_tax_pct > 0
            deductable_operating_expenses_series_bau = copy(total_operating_expenses_bau)
        else
            deductable_operating_expenses_series_bau = zeros(years)
        end

        operating_expenses_after_tax_bau = total_operating_expenses_bau - deductable_operating_expenses_series_bau +
                    deductable_operating_expenses_series_bau * (1 - p.s.financial.offtaker_tax_pct)
        free_cashflow_bau = operating_expenses_after_tax_bau + total_cash_incentives_bau
        free_cashflow_bau = append!([0.0], free_cashflow_bau)
        r["offtaker_annual_free_cashflows"] = round.(free_cashflow, digits=2)
        r["offtaker_discounted_annual_free_cashflows"] = [round(
            v / ((1 + p.s.financial.offtaker_discount_pct)^(yr-1)),
            digits=2) for (yr, v) in enumerate(r["offtaker_annual_free_cashflows"])]
        r["offtaker_annual_free_cashflows_bau"] = round.(free_cashflow_bau, digits=2)
        r["offtaker_discounted_annual_free_cashflows_bau"] = [round(
            v / ((1 + p.s.financial.offtaker_discount_pct)^(yr-1)),
            digits=2) for (yr, v) in enumerate(free_cashflow_bau)]
        # difference optimal and BAU
        net_free_cashflow = free_cashflow - free_cashflow_bau
        r["internal_rate_of_return"] = irr(net_free_cashflow)
        cumulative_cashflow = cumsum(net_free_cashflow)
    end

    # At this point we have the cumulative_cashflow for the developer or offtaker so the payback calculation is the same
    if cumulative_cashflow[end] < 0
        # case where the system does not pay itself back in the analysis period, do not caculate SPP and IRR
        return r
    end

    for i in 2:years
        # add years where the cumulative cashflow is negative
        if cumulative_cashflow[i] < 0
            r["simple_payback_years"] += 1
        # fractionally add years where the cumulative cashflow became positive
        elseif cumulative_cashflow[i - 1] < 0 && cumulative_cashflow[i] > 0
            r["simple_payback_years"] += -(cumulative_cashflow[i - 1] / net_free_cashflow[i])
        # skip years where cumulative cashflow is positive and the previous year's is too
        end
    end
    r["simple_payback_years"] = round(r["simple_payback_years"], digits=2)

    return r
end

"""
    update_metrics(m::Metrics, p::REoptInputs, tech::AbstractTech, tech_name::String, results::Dict, third_party::Bool)

Update the Metrics struct for the given `tech`
"""
function update_metrics(m::Metrics, p::REoptInputs, tech::AbstractTech, tech_name::String, results::Dict, third_party::Bool)
    total_kw = results[tech_name]["size_kw"]
    existing_kw = :existing_kw in fieldnames(typeof(tech)) ? tech.existing_kw : 0
    new_kw = total_kw - existing_kw
    capital_cost = new_kw * tech.installed_cost_per_kw

    # owner is responsible for both new and existing PV maintenance in optimal case
    if third_party
        annual_om = -1 * new_kw * tech.om_cost_per_kw
    else
        annual_om = -1 * total_kw * tech.om_cost_per_kw
    end
    years = p.s.financial.analysis_years
    escalate_om(val) = [val * (1 + p.s.financial.om_cost_escalation_pct)^yr for yr in 1:years]
    m.om_series += escalate_om(annual_om)
    m.om_series_bau += escalate_om(-1 * existing_kw * tech.om_cost_per_kw)

    # incentive calculations, in the spreadsheet utility incentives are applied first
    utility_ibi = minimum([capital_cost * tech.utility_ibi_pct, tech.utility_ibi_max])
    utility_cbi = minimum([new_kw * tech.utility_rebate_per_kw, tech.utility_rebate_max])
    state_ibi = minimum([(capital_cost - utility_ibi - utility_cbi) * tech.state_ibi_pct, tech.state_ibi_max])
    state_cbi = minimum([new_kw * tech.state_rebate_per_kw, tech.state_rebate_max])
    federal_cbi = new_kw * tech.federal_rebate_per_kw
    ibi = utility_ibi + state_ibi
    cbi = utility_cbi + federal_cbi + state_cbi
    m.total_ibi_and_cbi += ibi + cbi

    # Production-based incentives
    pbi_series = Float64[]
    pbi_series_bau = Float64[]
    existing_energy_bau = third_party ? get(results[tech_name], "year_one_energy_produced_kwh_bau", 0) : 0
    for yr in range(0, stop=years-1)
        if yr < tech.production_incentive_years
            degredation_pct = :degradation_pct in fieldnames(typeof(tech)) ? (1 - tech.degradation_pct)^yr : 1.0
            base_pbi = minimum([
                tech.production_incentive_per_kwh * (results[tech_name]["year_one_energy_produced_kwh"] - existing_energy_bau) * degredation_pct,
                tech.production_incentive_max_benefit * degredation_pct
            ])
            base_pbi_bau = minimum([
                tech.production_incentive_per_kwh * get(results[tech_name], "year_one_energy_produced_kwh_bau", 0) * degredation_pct,
                tech.production_incentive_max_benefit * degredation_pct
            ])
            push!(pbi_series, base_pbi)
            push!(pbi_series_bau, base_pbi_bau)
        else
            push!(pbi_series, 0.0)
            push!(pbi_series_bau, 0.0)
        end
    end
    m.total_pbi += pbi_series
    m.total_pbi_bau += pbi_series_bau

    # Federal ITC
    # NOTE: bug in v1 has the ITC within the `if tech.macrs_option_years in [5 ,7]` block.
    # NOTE: bug in v1 reduces the federal_itc_basis with the federal_cbi, which is incorrect
    federal_itc_basis = capital_cost - state_ibi - utility_ibi - state_cbi - utility_cbi
    federal_itc_amount = tech.federal_itc_pct * federal_itc_basis
    m.federal_itc += federal_itc_amount

    # Depreciation
    if tech.macrs_option_years in [5 ,7]
        schedule = []
        if tech.macrs_option_years == 5
            schedule = p.s.financial.macrs_five_year
        elseif tech.macrs_option_years == 7
            schedule = p.s.financial.macrs_seven_year
        end

        macrs_bonus_basis = federal_itc_basis - federal_itc_basis * tech.federal_itc_pct * tech.macrs_itc_reduction
        macrs_basis = macrs_bonus_basis * (1 - tech.macrs_bonus_pct)

        depreciation_schedule = zeros(years)
        for (i, r) in enumerate(schedule)
            if i < length(depreciation_schedule)
                depreciation_schedule[i] = macrs_basis * r
            end
        end
        depreciation_schedule[1] += (tech.macrs_bonus_pct * macrs_bonus_basis)
        m.total_depreciation += depreciation_schedule
    end
    nothing
end

function npv(cashflows::AbstractArray{Float64, 1}, rate::Float64)
    years = collect(0:length(cashflows)-1)
    return sum( cashflows ./ (1+rate).^years)
end

function irr(cashflows::AbstractArray{Float64, 1})
    if npv(cashflows, 0.0) < 0
        return 0.0
    end
    f(r) = npv(cashflows, r)
    rate = 0.0
    try
        rate = fzero(f, [0.0, 0.99])
    finally
        return round(rate, digits=2)
    end
end


export load_obj

# ------------------------- #
# - Parse OBJ & MTL Files - #
# ------------------------- #
"""
    triangulate_faces(vertices::Vector, texture_coordinates::Vector,
                      faces::Vector, material_map::Dict)

Triangulates the `faces` and converts it into valid [`Triangle`](@ref) objects.

!!! warning
    Avoid using this function in itself. It is designed to be used internally
    by the [`load_obj`](@ref) function.
"""
function triangulate_faces(vertices::Vector, texture_coordinates::Vector,
                           faces::Vector, material_map::Dict)
    scene = Vector{Triangle}()
    for face in faces
        for i in 2:(length(face[1]) - 1)
            if isnothing(face[2])
                uv_coordinates = nothing
            else
                uv_coordinates = [[texture_coordinates[face[2][1]]...],
                                  [texture_coordinates[face[2][i]]...],
                                  [texture_coordinates[face[2][i + 1]]...]]
            end
            mat = Material(;uv_coordinates = uv_coordinates,
                           material_map[face[3]]...)
            push!(scene, Triangle(deepcopy(vertices[face[1][1]]),
                                  deepcopy(vertices[face[1][i]]),
                                  deepcopy(vertices[face[1][i + 1]]),
                                  mat))
        end
    end
    try
        scene_stable = Vector{typeof(scene[1])}()
        for s in scene
            push!(scene_stable, s)
        end
        return scene_stable
    catch e
        # If all the triangles are not of the same type return the non-infered
        # version of the scene. In this case type inference for `raytrace` will
        # also fail
        @warn "Could not convert the triangles to the same type. Type inference for raytrace will fail"
        return scene
    end
end

"""
    parse_mtllib!(file, material_map, outtype)

Parses `.mtl` file and creates a map from the material name to
[`Material`](@ref) objects.

Not all fields of the `mtl` file are parsed. We only parse :
`newmtl`, `Ka`, `Kd`, `Ks`, `Ns`, `d`, `Tr`, `map_Kd`, `map_Ks`,
and `map_Ka`.
"""
function parse_mtllib!(file, material_map, outtype)
    color_diffuse = Vec3(outtype(1.0f0))
    color_ambient = Vec3(outtype(1.0f0))
    color_specular = Vec3(outtype(1.0f0))
    specular_exponent = outtype(50.0f0)
    reflection = outtype(0.5f0)
    texture_ambient = nothing
    texture_diffuse = nothing
    texture_specular = nothing
    last_mat = "RayTracer Default"
    if isnothing(file)
        material_map[last_mat] = (color_diffuse = color_diffuse,
                                  color_ambient = color_ambient,
                                  color_specular = color_specular,
                                  specular_exponent = specular_exponent,
                                  reflection = reflection,
                                  texture_ambient = texture_ambient,
                                  texture_diffuse = texture_diffuse,
                                  texture_specular = texture_specular)
        return nothing
    end
    for line in eachline(file)
        wrds = split(line)
        isempty(wrds) && continue
        if wrds[1] == "newmtl"
            material_map[last_mat] = (color_diffuse = color_diffuse,
                                      color_ambient = color_ambient,
                                      color_specular = color_specular,
                                      specular_exponent = specular_exponent,
                                      reflection = reflection,
                                      texture_ambient = texture_ambient,
                                      texture_diffuse = texture_diffuse,
                                      texture_specular = texture_specular)
            last_mat = wrds[2]
            # In case any of these values are not defined for the material
            # we shall use the default values
            color_diffuse = Vec3(outtype(1.0f0))
            color_ambient = Vec3(outtype(1.0f0))
            color_specular = Vec3(outtype(1.0f0))
            specular_exponent = outtype(50.0f0)
            reflection = outtype(0.5f0)
            texture_ambient = nothing
            texture_diffuse = nothing
            texture_specular = nothing
        elseif wrds[1] == "Ka"
            color_ambient = Vec3(parse.(outtype, wrds[2:4])...)
        elseif wrds[1] == "Kd"
            color_diffuse = Vec3(parse.(outtype, wrds[2:4])...)
        elseif wrds[1] == "Ks"
            color_specular = Vec3(parse.(outtype, wrds[2:4])...)
        elseif wrds[1] == "Ns"
            specular_exponent = parse(outtype, wrds[2])
        elseif wrds[1] == "d"
            reflection = parse(outtype, wrds[2])
        elseif wrds[1] == "Tr"
            reflection = 1 - parse(outtype, wrds[2])
        elseif wrds[1] == "map_Ka"
            texture_file = "$(rsplit(file, '/', limit = 2)[1])/$(wrds[2])"
            texture_ambient = Vec3([outtype.(permutedims(channelview(load(texture_file)),
                                                         (3, 2, 1)))[:,end:-1:1,i]
                                    for i in 1:3]...)
        elseif wrds[1] == "map_Kd"
            texture_file = "$(rsplit(file, '/', limit = 2)[1])/$(wrds[2])"
            texture_diffuse = Vec3([outtype.(permutedims(channelview(load(texture_file)),
                                                         (3, 2, 1)))[:,end:-1:1,i]
                                    for i in 1:3]...)
        elseif wrds[1] == "map_Ks"
            texture_file = "$(rsplit(file, '/', limit = 2)[1])/$(wrds[2])"
            texture_specular = Vec3([outtype.(permutedims(channelview(load(texture_file)),
                                                          (3, 2, 1)))[:,end:-1:1,i]
                                     for i in 1:3]...)
        end
    end
    material_map[last_mat] = (color_diffuse = color_diffuse,
                              color_ambient = color_ambient,
                              color_specular = color_specular,
                              specular_exponent = specular_exponent,
                              texture_ambient = texture_ambient,
                              texture_diffuse = texture_diffuse,
                              texture_specular = texture_specular)
    return nothing
end

"""
    load_obj(file; outtype = Float32)

Parser for `obj` file. It returns a set of Triangles that make up the scene.

Only the following things are parsed as of now: `v`, `vt`, `vn`, `f`, `usemtl`
and `mtllib`.
"""
function load_obj(file; outtype = Float32)
    vertices = Vector{Vec3{Vector{outtype}}}()
    texture_coordinates = Vector{Tuple}()
    normals = Vector{Vec3{Vector{outtype}}}()
    faces = Vector{Tuple{Vector{Int}, Union{Vector{Int}, Nothing}, String}}()
    material_map = Dict{String, Union{NamedTuple, Nothing}}()
    last_mat = "RayTracer Default"
    mtllib = nothing
    for line in eachline(file)
        wrds = split(line)
        isempty(wrds) && continue
        if wrds[1] == "v" # Vertices
            push!(vertices, Vec3(parse.(outtype, wrds[2:4])...))
        elseif wrds[1] == "vt" # Texture Coordinates
            push!(texture_coordinates, tuple(parse.(outtype, wrds[2:3])...))
        elseif wrds[1] == "vn" # Normal
            push!(normals, Vec3(parse.(outtype, wrds[2:4])...))
        elseif wrds[1] == "f" # Faces
            # Currently we shall only be concerned with the vertices of the face
            # and safely throw away vertex normal information
            fstwrd = split(wrds[2], '/')
            texture_c = isempty(fstwrd[2]) ? nothing : [parse(Int, fstwrd[2])]
            @assert length(fstwrd) == 3 "Incorrect number of /'s in the obj file"
            push!(faces, ([parse(Int, fstwrd[1])], texture_c, last_mat))
            for wrd in wrds[3:end]
                splitwrd = split(wrd, '/')
                @assert length(splitwrd) == 3 "Incorrect number of /'s in the obj file"
                push!(faces[end][1], parse(Int, splitwrd[1]))
                !isnothing(texture_c) && push!(faces[end][2], parse(Int, splitwrd[2]))
            end
        elseif wrds[1] == "usemtl" # Key for parsing mtllib file
            last_mat = wrds[2]
            material_map[last_mat] = nothing
        elseif wrds[1] == "mtllib" # Material file
            mtllib = "$(rsplit(file, '/', limit = 2)[1])/$(wrds[2])"
        end
    end

    if !isnothing(mtllib)
        parse_mtllib!(mtllib, material_map, outtype)
    else
        parse_mtllib!(nothing, material_map, outtype)
    end

    return triangulate_faces(vertices, texture_coordinates, faces, material_map)
end


using JLD2

@doc """
    runMC(param::Parameter)
    runMC(params::AbstractArray{Parameter}
          ;
          parallel::Bool=false,
          autoID::Bool=true)

Runs Monte Carlo simulation(s) and returns calculated observables.

If a checkpoint file named `"\$(param["Checkpoint Filename Prefix"])_\$(param["ID"]).jld2"` exists and
`param["Checkpoint Interval"] > 0.0`, `runMC` loads this file and restarts the pending simulation.

# Keyward aruguments
- `autoID`: If true, `"ID"`s will be set (overwritten) as `params[i]["ID"] = i`.
- `parallel`: If true, runs simulations in parallel (uses `pmap` instead of `map`).

# Required keys in `param`
- "Model"
- "Update Method"

# Optional keys in `param`
- "MCS": The number of Monte Carlo steps after thermalization
    - Default: `8192`
- "Thermalization": The number of Monte Carlo steps for thermalization
    - Default: `MCS>>3`
- "Seed": The initial seed of the random number generator, `MersenneTwister`
    - Default: determined randomly (see `Random.srand`)
- "Checkpoint Filename Prefix": See above document.
    - Default: `"cp"`
- "ID": See above document.
    - Default: `0`
- "Checkpoint Interval": Time interval between writing checkpoint file in seconds.
    - Default: `0.0`, this means that NO checkpoint file will be loaded and saved.
"""
function runMC(params::AbstractArray{T}; parallel::Bool=false, autoID::Bool=true) where T<:Dict
    map_fn = ifelse(parallel, pmap, map)
    return map_fn(enumerate(params)) do idp
        id,p = idp
        if autoID
            p["ID"] = id
        end
        return runMC(p)
    end
end

function runMC(param::Parameter)
    model = param["Model"](param)
    if "Seed" in keys(param)
        srand(model, param["Seed"])
    end
    ret = runMC(model, param)
    return ret
end

function runMC(model, param::Parameter)
    MODEL = typeof(model)
    verbose = get(param, "Verbose", false) :: Bool
    if verbose
        println("Start: ", param)
    end
    cp_filename = @sprintf("%s_%d.jld2",
                           get(param, "Checkpoint Filename Prefix", "cp")::String,
                           get(param, "ID", 0)::Int)
    cp_interval = get(param, "Checkpoint Interval", 0.0) :: Float64
    if VERSION > v"0.6.4" && cp_interval != 0.0
        Compat.@info """"Checkpoint Interval" is set to 0.0 automatically since current JLD2.jl cannot save/load Random.MersenneTwister object properly in julia-0.7"""
        cp_interval = 0.0
    end
    tm = time()

    MCS = get(param, "MCS", 8192) :: Int
    Therm = get(param, "Thermalization", MCS>>3) :: Int

    mcs = 0
    MCS += Therm
    obs = BinningObservableSet()
    makeMCObservable!(obs, "Time per MCS")
    makeMCObservable!(obs, "MCS per Second")

    if cp_interval > 0.0 && ispath(cp_filename)
        jld = jldopen(cp_filename)
        model = jld["model"] :: MODEL
        obs = jld["obs"] :: BinningObservableSet
        mcs = convert(Int, jld["mcs"]) :: Int
        close(jld)
    end

    if "UpdateMethod" in keys(param)
        warn("\"UpdateMethod\" is deprecated. Use instead \"Update Method\".")
        param["Update Method"] = param["UpdateMethod"] :: Function
    end
    update! = param["Update Method"] :: Function
    estimator = get(param, "Estimator", default_estimator(model, update!)) :: Function
    p = convert_parameter(model, param)

    while mcs < MCS
        if mcs < Therm
            update!(model, p...)
        else
            t = @elapsed begin
                st = update!(model,p...)
                localobs = estimator(model, p..., st)
            end
            obs["Time per MCS"] << t
            obs["MCS per Second"] << 1.0/t
            accumulateObservables!(model, obs, localobs)
        end
        mcs += 1
        if cp_interval > 0.0 && time() - tm > cp_interval
            jld = jldopen(cp_filename, "w")
            jld["model"] = model
            jld["obs"] = obs
            jld["mcs"] = mcs
            close(jld)
            tm += cp_interval
         end
    end

    if cp_interval > 0.0
        jld = jldopen(cp_filename, "w")
        jld["model"] = model
        jld["obs"] = obs
        jld["mcs"] = mcs
        close(jld)
    end

    jk = postproc(model, param, obs)

    if verbose
        println("Finish: ", param)
    end
    return jk
end

@doc """
    accumulateObservables!(model, obs::MCObservableSet, localobs::Dict)

Accumulates `localobs` into `obs`. For example, `obs["Energy"] << localobs["Energy"]`.
"""
function accumulateObservables!(::Model, obs::MCObservableSet, localobs::Measurement)
    if length(obs) < 3
        @inbounds for key in keys(localobs)
            makeMCObservable!(obs, key)
            obs[key] << localobs[key]
        end
    else
        @inbounds for key in keys(localobs)
            obs[key] << localobs[key]
        end
    end
    return obs
end

@doc """
    postproc(model::Model, param::Parameter, obs::MCObservableSet)

Post process of observables. For example, Specific heat will be calculated from energy, energy^2, and temperature.
"""
function postproc end

@doc doc"""
    postproc(model::Union{Ising, Potts}, param::Parameter, obs::MCObservableSet)

# Observables to be calculated
In the following, $m$ is total magnetization per site and $\epsilon$ is total energy per site.

- `"Binder Ratio"`
    - $R := \frac{\left \langle m^4 \right \rangle}{\left \langle m^2 \right\rangle^2}$
- `"Susceptibility"`
    - $\chi := \frac{N}{T}\left(\left\langle m^2\right\rangle\right)$
- `"Connected Susceptibility"`
    - $\frac{N}{T}\left(\left\langle m^2\right\rangle - \left\langle |m| \right\rangle^2\right)$
- `"Specific Heat"`
    - $\frac{N}{T^2}\left(\left\langle \epsilon^2\right\rangle - \left\langle \epsilon \right\rangle^2\right)$
"""
function postproc(model::Union{Ising, Potts}, param::Parameter, obs::MCObservableSet)
    nsites = numsites(model)
    T = param["T"] :: Float64
    beta = 1.0/T

    jk = jackknife(obs)
    jk["Binder Ratio"] = jk["Magnetization^4"] / (jk["Magnetization^2"]^2)
    jk["Susceptibility"] = (nsites*beta)*jk["Magnetization^2"]
    jk["Connected Susceptibility"] = (nsites*beta)*(jk["Magnetization^2"] - jk["|Magnetization|"]^2)
    jk["Specific Heat"] = (nsites*beta*beta)*(jk["Energy^2"] - jk["Energy"]^2)
    return jk
end

@doc doc"""
    postproc(model::Union{Clock, XY}, param::Parameter, obs::MCObservableSet)

# Observables to be calculated
In the following, $m$ is total magnetization per site and $\epsilon$ is total energy per site.

- `"Binder Ratio x"`
    - $\frac{\left \langle m_x^4 \right \rangle}{\left \langle m_x^2 \right\rangle^2}$
- `"Binder Ratio y"`
    - $\frac{\left \langle m_y^4 \right \rangle}{\left \langle m_y^2 \right\rangle^2}$
- `"Binder Ratio"`
    - $\frac{\left \langle |m|^4 \right \rangle}{\left \langle |m|^2 \right\rangle^2}$
- `"Susceptibility x"`
    - $\frac{N}{T}\left(\left\langle m_x^2\right\rangle\right)$
- `"Susceptibility y"`
    - $\frac{N}{T}\left(\left\langle m_y^2\right\rangle\right)$
- `"Susceptibility y"`
    - $\frac{N}{T}\left(\left\langle |m|^2\right\rangle\right)$
- `"Connected Susceptibility x"`
    - $\frac{N}{T}\left(\left\langle m_x^2\right\rangle - \left\langle |m_x| \right\rangle^2\right)$
- `"Connected Susceptibility y"`
    - $\frac{N}{T}\left(\left\langle m_y^2\right\rangle - \left\langle |m_y| \right\rangle^2\right)$
- `"Connected Susceptibility"`
    - $\frac{N}{T}\left(\left\langle |m|^2\right\rangle - \left\langle |m| \right\rangle^2\right)$
- `"Specific Heat"`
    - $\frac{N}{T^2}\left(\left\langle \epsilon^2\right\rangle - \left\langle \epsilon \right\rangle^2\right)$
"""
function postproc(model::Union{Clock, XY}, param::Parameter, obs::MCObservableSet)
    nsites = numsites(model)
    T = param["T"]
    beta = 1.0/T

    jk = jackknife(obs)
    jk["Binder Ratio x"] = jk["Magnetization x^4"] / (jk["Magnetization x^2"]^2)
    jk["Binder Ratio y"] = jk["Magnetization y^4"] / (jk["Magnetization y^2"]^2)
    jk["Binder Ratio"] = jk["|Magnetization|^4"] / (jk["|Magnetization|^2"]^2)
    jk["Susceptibility x"] = (nsites*beta)*jk["Magnetization x^2"]
    jk["Susceptibility y"] = (nsites*beta)*jk["Magnetization y^2"]
    jk["Susceptibility"] = (nsites*beta)*jk["|Magnetization|^2"]
    jk["Connected Susceptibility x"] = (nsites*beta)*(jk["Magnetization x^2"] - jk["|Magnetization x|"]^2)
    jk["Connected Susceptibility y"] = (nsites*beta)*(jk["Magnetization y^2"] - jk["|Magnetization y|"]^2)
    jk["Connected Susceptibility"] = (nsites*beta)*(jk["|Magnetization|^2"] - jk["|Magnetization|"]^2)
    jk["Specific Heat"] = (nsites*beta*beta)*(jk["Energy^2"] - jk["Energy"]^2)
    return jk
end

@doc doc"""
    postproc(model::QuantumXXZ, param::Parameter, obs::MCObservableSet)

# Observables to be calculated
In the following, $s$ is sign of weight, $m$ is total magnetization per site,
and $\epsilon$ is total energy per site.

- `"Magnetization"`
    - $\left\langle m s\right\rangle\Big/\left\langle s \right\rangle$
- `"|Magnetization|"`
    - $\left\langle |m| s\right\rangle\Big/\left\langle s \right\rangle$
- `"Magnetization^2"`
    - $\left\langle m^2 s\right\rangle\Big/\left\langle s \right\rangle$
- `"Magnetization^4"`
    - $\left\langle m^4 s\right\rangle\Big/\left\langle s \right\rangle$
- `"Energy"`
    - $\left\langle \epsilon s\right\rangle\Big/\left\langle s \right\rangle$
- `"Energy^2"`
    - $\left\langle \epsilon^2 s\right\rangle\Big/\left\langle s \right\rangle$
- `"Binder Ratio"`
    - $\frac{\left \langle m^4 \right \rangle}{\left \langle m^2 \right\rangle^2}$
- `"Susceptibility"`
    - $\frac{N}{T}\left(\left\langle m^2\right\rangle\right)$
- `"Connected Susceptibility"`
    - $\frac{N}{T}\left(\left\langle m^2\right\rangle - \left\langle |m| \right\rangle^2\right)$
- `"Specific Heat"`
    - $\frac{N}{T^2}\left(\left\langle \epsilon^2\right\rangle - \left\langle \epsilon \right\rangle^2\right)$
"""
function postproc(model::QuantumXXZ, param::Parameter, obs::MCObservableSet)
    nsites = numsites(model)
    T = param["T"]
    beta = 1.0/T

    jk = jackknife(obs)

    for oname in ("Magnetization", "|Magnetization|",
                  "Magnetization^2", "Magnetization^4",
                  "Energy", "Energy^2",
                 )
        jk[oname] = jk["Sign * $oname"] / jk["Sign"]
    end

    jk["Binder Ratio"] = jk["Magnetization^4"] / (jk["Magnetization^2"]^2)
    jk["Susceptibility"] = (nsites*beta)*jk["Magnetization^2"]
    jk["Connected Susceptibility"] = (nsites*beta)*(jk["Magnetization^2"] - jk["|Magnetization|"]^2)
    jk["Specific Heat"] = (nsites*beta*beta)*(jk["Energy^2"] - jk["Energy"]^2)
    return jk
end


function similar_cells( cell_connectivity1::Vector{Int},
                        cell_type1::Int,
                        cell_connectivity2::Union{Tuple{Vararg{Int}}, Vector{Int}},
                        cell_type2::Int
                      )
    for k in cell_connectivity2
        in(k, cell_connectivity1) || return false
    end
    cell_type1 == cell_type2 || return false
    return true
end

function coherent(a::T, b::T) where {T<:AbstractVTKMultiblockData}
    return same_ordered_geometry_shape(a,b) && same_data_shape(a,b)
end
function coherent(a::T, b::T) where {T<:AbstractVTKSimpleData}
    return same_geometry_shape(a, b) && same_data_shape(a, b)
end
function coherent(dataset::AbstractTimeSeriesVTKData)
    length(dataset) == 0 && return true
    block1 = dataset[1]
    for block in dataset[2:end]
        coherent(block1, block) || return false
    end
    return true
end
function coherent(a::T, b::T) where {T<:AbstractTimeSeriesVTKData}
    length(a) == length(b) &&
    coherent(a) && coherent(b) && begin
        _out = true
        for i in 1:length(a)
            (_out = coherent(a[i], b[i])) || break
        end
        _out
    end || return false
    return true
end
coherent(a::AbstractVTKData, b::AbstractVTKData) = false

extents(dataset::VTKStructuredData) = Base.tail(size(dataset.point_coords))
function extents(dataset::VTKRectilinearData)
    return ntuple(i->length(dataset.point_coords[i]), Val(length(dataset.point_coords)))
end
extents(dataset::VTKUniformRectilinearData) = dataset.extents
extents(dataset::AbstractVTKStructuredData, ind::Int) = extents(dataset)[ind]
extents(dataset::AbstractVTKStructuredData, ind::Int...) = extents.(Ref(dataset), ind)
cell_extents(dataset::AbstractVTKStructuredData) = ntuple(i -> extents(dataset, i) .- 1, dim(dataset))
cell_extents(dataset::AbstractVTKStructuredData, ind::Int) = cell_extents(dataset)[ind]
cell_extents(dataset::AbstractVTKStructuredData, ind::Int...) = cell_extents.(Ref(dataset), ind)

dim(dataset::AbstractVTKUnstructuredData) = size(dataset.point_coords, 1)
dim(dataset::AbstractVTKStructuredData) = length(extents(dataset))
function dim(dataset::AbstractVTKMultiblockData)
    return maximum(i -> dim(dataset[i]), 1:length(dataset))
end

num_of_points(dataset::AbstractVTKUnstructuredData) = size(dataset.point_coords, 2)
num_of_points(dataset::AbstractVTKStructuredData) = prod(extents(dataset))
num_of_points(dataset::AbstractVTKMultiblockData) = sum(num_of_points(i) for i in dataset)
num_of_points(dataset::AbstractTimeSeriesVTKData, ind::Int=1) = num_of_points(dataset[ind])

num_of_cells(dataset::AbstractVTKUnstructuredData) = length(dataset.cell_types)
num_of_cells(dataset::AbstractVTKStructuredData) = prod(extents(dataset) .- 1)
num_of_cells(dataset::AbstractVTKMultiblockData) = sum(num_of_cells(i) for i in dataset)
num_of_cells(dataset::AbstractTimeSeriesVTKData, ind::Int=1) = num_of_cells(dataset[ind])

num_of_point_vars(dataset::AbstractVTKSimpleData) = length(dataset.point_data)
num_of_cell_vars(dataset::AbstractVTKSimpleData) = length(dataset.cell_data)

function cell_type(dataset::Union{VTKUnstructuredData, VTKPolyData}, ind::Int)
    return dataset.cell_types[ind]
end
function cell_type(dataset::Union{VTKStructuredData, VTKRectilinearData}, ind::Int)
    if ind < 1 || ind > num_of_cells(dataset)
        throw("Out of bounds.")
    else
        return dim(dataset) == 2 ? 9 : 12
    end
end
function cell_type(dataset::VTKUniformRectilinearData, ind::Int)
    if ind < 1 || ind > num_of_cells(dataset)
        throw("Out of bounds.")
    else
        return dim(dataset) == 2 ? 8 : 11
    end
end
function cell_type(dataset::AbstractVTKStructuredData, ind::Tuple{Vararg{Int}})
    return cell_type(dataset, (LinearIndices(cell_extents(dataset)))[ind...])
end

function cell_connectivity(dataset::AbstractVTKUnstructuredData, cell_ind::Int)
    return dataset.cell_connectivity[cell_ind]
end
function cell_connectivity(dataset::T, cell_ind::Tuple{Vararg{Int}}) where {T<:AbstractVTKStructuredData}
    pextents = extents(dataset)
    return cell_connectivity(T, pextents, cell_ind)
end

function cell_connectivity(dataset::T, cell_ind) where {T<:AbstractVTKStructuredData}
    return cell_connectivity(T, extents(dataset), cell_ind)
end

function cell_connectivity(T::DataType, pextents::NTuple{N,Int}, cell_ind::NTuple{N,Int}) where {N}
    corner_point_ind = (LinearIndices(pextents))[cell_ind...]
    if N == 2
        #1 <= cell_ind <= num_of_cells(dataset) || throw("Out of bounds.")
        if T <: VTKUniformRectilinearData
            return [corner_point_ind, corner_point_ind + 1, corner_point_ind + pextents[1],
                    corner_point_ind + pextents[1] + 1]
        else
            return [corner_point_ind, corner_point_ind + 1, corner_point_ind + pextents[1] + 1,
                    corner_point_ind + pextents[1]]
        end
    elseif N == 3
        #1 <= cell_ind <= num_of_cells(dataset) || throw("Out of bounds.")
        if T <: VTKUniformRectilinearData
            return [corner_point_ind, corner_point_ind + 1, corner_point_ind + pextents[1],
                    corner_point_ind + pextents[1] + 1,
                    corner_point_ind + pextents[1] * pextents[2],
                    corner_point_ind + pextents[1] * pextents[2] + 1,
                    corner_point_ind + pextents[1] * pextents[2] + pextents[1],
                    corner_point_ind + pextents[1] * pextents[2] + pextents[1] + 1]
        else
            return [corner_point_ind, corner_point_ind + 1, corner_point_ind + pextents[1] + 1,          corner_point_ind + pextents[1],
                    corner_point_ind + pextents[1] * pextents[2],
                    corner_point_ind + pextents[1] * pextents[2] + 1,
                    corner_point_ind + pextents[1] * pextents[2] + pextents[1] + 1, corner_point_ind + pextents[1] * pextents[2] + pextents[1]]
        end
    end

    throw("Invalid mesh dimension.")
end

function has_var(dataset::AbstractStaticVTKData, var_name::String)
    if haskey(dataset.point_data, var_name)
        return true, "Point"
    elseif haskey(dataset.cell_data, var_name)
        return true, "Cell"
    else
        return false, ""
    end
end

function var_dim(dataset::AbstractVTKUnstructuredData, var_name::String, var_type::String="")
    if var_type == "Point" && haskey(dataset.point_data, var_name)
        if length(size(dataset.point_data[var_name])) == 1
            return 1
        else
            return length(dataset.point_data[var_name][:,1])
        end
    elseif var_type == "Cell" && haskey(dataset.cell_data, var_name)
        if length(size(dataset.cell_data[var_name])) == 1
            return 1
        else
            return length(dataset.cell_data[var_name][:,1])
        end
    elseif var_type == ""
        try
            return var_dim(dataset, var_name, "Point")
        catch
            return var_dim(dataset, var_name, "Cell")
        end
    else
        throw("Variable $var_name doesn't exist, please use has_var to check if the variable exists before using this function.")
    end
end

function var_dim(dataset::AbstractVTKStructuredData, var_name::String, var_type::String="")
    if var_type == "Point" && haskey(dataset.point_data, var_name)
        if length(size(dataset.point_data[var_name])) == length(extents(dataset))
            return 1
        else
            return size(dataset.point_data[var_name], 1)
        end
    elseif var_type == "Cell" && haskey(dataset.cell_data, var_name)
        if length(size(dataset.cell_data[var_name])) == length(extents(dataset))
            return 1
        else
            return size(dataset.cell_data[var_name], 1)
        end
    elseif var_type == ""
        try
            return var_dim(dataset, var_name, "Point")
        catch
            return var_dim(dataset, var_name, "Cell")
        end
    else
        throw("Variable $var_name doesn't exist, please use has_var to check if the variable exists before using this function.")
    end
end

function vtk_cell_type_name(dataset::AbstractVTKStructuredData, cell_id)
    return VTK_CELL_TYPE[cell_type(dataset, cell_id)]
end
function is_homogeneous(dataset::AbstractVTKUnstructuredData)
    begin
        out = true
        for i in 2:num_of_cells(dataset)
            (out = cell_type(dataset, 1) == cell_type(dataset, i)) || break
        end
        out
    end || return false
    return true
end

is_homogeneous(dataset::AbstractVTKStructuredData) = true

function is_homogeneous(dataset::AbstractVTKMultiblockData)
    return all(i -> typeof(dataset[1]) == typeof(dataset[i]), 2:length(dataset))
end
function is_homogeneous(dataset::AbstractTimeSeriesVTKData)
    length(dataset) == 0 ||
    is_homogeneous(dataset[1]) && begin
        out = true
        i = dataset[1]
        T = typeof(i)
        for j in dataset
            (out = isa(j, T)) || break
            if T <: AbstractVTKMultiblockData || T <: AbstractVTKUnstructuredData
                (out = same_ordered_geometry_shape(i,j)) || break
            else
                (out = same_geometry_shape(i,j)) || break
            end
        end
        out
    end || return false
    return true
end

function get_cell_ids(dataset::AbstractVTKUnstructuredData, cell_types::Vector{Int})
    cell_ids = Int[]
    for (i, c) in enumerate(dataset.cell_types)
        if in(c, cell_types)
            push!(cell_ids, i)
        end
    end
    return cell_ids
end

function get_lowest_index(cell_connectivity)
    return minimum(i -> minimum(cell_connectivity[i]), 1:length(cell_connectivity))
end
function get_highest_index(cell_connectivity)
    return maximum(i -> maximum(cell_connectivity[i]), 1:length(cell_connectivity))
end
function is_valid_cell(cell_connectivity, cell_type)
    return VTK_CELL_TYPE[cell_type].nodes == -1 ||
            length(_cell_connectivity) == VTK_CELL_TYPE[cell_type].nodes
end
num_of_blocks(dataset::AbstractVTKMultiblockData) = length(dataset.blocks)

timespan(dataset::VTKTimeSeriesData) = dataset.timemarkers[end] - dataset.timemarkers[1]

num_of_timesteps(dataset::VTKTimeSeriesData) = length(dataset.timemarkers)

triangular(dataset::AbstractVTKUnstructuredData) = all(i -> i == 5, dataset.cell_types)

function bb(dataset::T) where {T<:AbstractVTKData}
    if T <: AbstractVTKUnstructuredData
        return bb_unstruct(dataset)
    elseif T <: VTKStructuredData
        return bb_struct(dataset)
    elseif T <: VTKRectilinearData
        return bb_rect(dataset)
    elseif T <: VTKUniformRectilinearData
        return bb_image(dataset)
    elseif T <: Union{AbstractVTKMultiblockData, AbstractTimeSeriesVTKData}
        return bb_multiblock_time(dataset)
    end
    throw("Unsupported type.")
end

function bb_unstruct(dataset::AbstractVTKUnstructuredData)
    _dim = dim(dataset)
    min_x = max_x = dataset.point_coords[1,1]
    min_y = max_y = dataset.point_coords[2,1]
    if _dim == 2
        for i in 2:num_of_points(dataset)
            if dataset.point_coords[1,i] > max_x
                max_x = dataset.point_coords[1,i]
            elseif dataset.point_coords[1,i] < min_x
                min_x = dataset.point_coords[1,i]
            end

            if dataset.point_coords[2,i] > max_y
                max_y = dataset.point_coords[2,i]
            elseif dataset.point_coords[2,i] < min_y
                min_y = dataset.point_coords[2,i]
            end
        end
        return (min_x, max_x, min_y, max_y)
    elseif _dim == 3
        min_z = max_z = dataset.point_coords[3,1]
        for i in 2:num_of_points(dataset)
            if dataset.point_coords[1,i] > max_x
                max_x = dataset.point_coords[1,i]
            elseif dataset.point_coords[1,i] < min_x
                min_x = dataset.point_coords[1,i]
            end

            if dataset.point_coords[2,i] > max_y
                max_y = dataset.point_coords[2,i]
            elseif dataset.point_coords[2,i] < min_y
                min_y = dataset.point_coords[2,i]
            end

            if dataset.point_coords[3,i] > max_z
                max_z = dataset.point_coords[3,i]
            elseif dataset.point_coords[3,i] < min_z
                min_z = dataset.point_coords[3,i]
            end
        end
        return (min_x, max_x, min_y, max_y, min_z, max_z)
    else
        throw("Invalid dimension.")
    end
end

@resumable function surface_cell_inds(dataset::AbstractVTKStructuredData)
    cextents = cell_extents(dataset)
    if dim(dataset) == 2
        i = 1
        for j in 1:cextents[2]
            @yield (i,j)
        end
        j = 1
        for i in 2:cextents[1]
            @yield (i,j)
        end

        i = cextents[1]
        for j in 2:cextents[2]
            @yield (i,j)
        end
        j = cextents[2]
        for i in 2:cextents[1]-1
            @yield (i,j)
        end
    else
        i = 1
        for j in 1:cextents[2], k in 1:cextents[3]
            @yield (i,j,k)
        end

        j = 1
        for i in 2:cextents[1], k in 1:cextents[3]
            @yield (i,j,k)
        end

        k = 1
        for i in 2:cextents[1], j in 2:cextents[2]
            @yield (i,j,k)
        end

        i = cextents[1]
        for j in 2:cextents[2], k in 2:cextents[3]
            @yield (i,j,k)
        end

        j = cextents[2]
        for i in 2:cextents[1]-1, k in 2:cextents[3]
            @yield (i,j,k)
        end

        k = cextents[3]
        for i in 2:cextents[1]-1, j in 2:cextents[3]-1
            @yield (i,j,k)
        end
    end
end

function bb_struct(dataset::VTKStructuredData)
    cextents = cell_extents(dataset)
    _dim = dim(dataset)

    min_x = max_x = dataset.point_coords[1,1]
    min_y = max_y = dataset.point_coords[2,1]
    if _dim == 2
        for (i,j) in surface_cell_inds(dataset)
            if dataset.point_coords[1,i] > max_x
                max_x = dataset.point_coords[1,i]
            elseif dataset.point_coords[1,i] < min_x
                min_x = dataset.point_coords[1,i]
            end

            if dataset.point_coords[2,i] > max_y
                max_y = dataset.point_coords[2,i]
            elseif dataset.point_coords[2,i] < min_y
                min_y = dataset.point_coords[2,i]
            end
        end
        return (min_x, max_x, min_y, max_y)
    elseif _dim == 3
        min_z = max_z = dataset.point_coords[3,1]
        for (i,j,k) in surface_cell_inds(dataset)
            if dataset.point_coords[1,i] > max_x
                max_x = dataset.point_coords[1,i]
            elseif dataset.point_coords[1,i] < min_x
                min_x = dataset.point_coords[1,i]
            end

            if dataset.point_coords[2,i] > max_y
                max_y = dataset.point_coords[2,i]
            elseif dataset.point_coords[2,i] < min_y
                min_y = dataset.point_coords[2,i]
            end

            if dataset.point_coords[3,i] > max_z
                max_z = dataset.point_coords[3,i]
            elseif dataset.point_coords[3,i] < min_z
                min_z = dataset.point_coords[3,i]
            end
        end
        return (min_x, max_x, min_y, max_y, min_z, max_z)
    else
        throw("Invalid dimension.")
    end
end

function bb_rect(dataset::VTKRectilinearData)
    min_x = dataset.point_coords[1][1]
    max_x = dataset.point_coords[1][end]
    min_y = dataset.point_coords[2][1]
    max_y = dataset.point_coords[2][end]

    if dim(dataset) == 2
        return (min_x, max_x, min_y, max_y)
    elseif dim(dataset) == 3
        min_z = dataset.point_coords[3][1]
        max_z = dataset.point_coords[3][end]
        return (min_x, max_x, min_y, max_y, min_z, max_z)
    else
        throw("Invalid dimension.")
    end
end

function bb_image(dataset::VTKUniformRectilinearData)
    min_x = dataset.origin[1]
    max_x = dataset.origin[1] + (dataset.extents[1]-1)*dataset.spacing[1]
    min_x = dataset.origin[2]
    max_x = dataset.origin[2] + (dataset.extents[2]-1)*dataset.spacing[2]

    if dim(dataset) == 2
        return (min_x, max_x, min_y, max_y)
    elseif dim(dataset) == 3
        min_x = dataset.origin[3]
        max_x = dataset.origin[3] + (dataset.extents[3]-1)*dataset.spacing[3]
        return (min_x, max_x, min_y, max_y, min_z, max_z)
    else
        throw("Invalid dimension.")
    end
end

function bb_multiblock_time(dataset::Union{AbstractVTKMultiblockData, AbstractTimeSeriesVTKData})
    _bbs = [bb(block) for block in dataset]
    min_x = minimum(i -> _bbs[i][1], 1:length(_bbs))
    max_x = maximum(i -> _bbs[i][2], 1:length(_bbs))
    min_y = minimum(i -> _bbs[i][3], 1:length(_bbs))
    max_y = maximum(i -> _bbs[i][4], 1:length(_bbs))

    _first = true
    min_z = max_z = 0
    for _bb in _bbs
        if length(_bb) == 6
            if _first
                min_z = _bb[5]
                max_z = _bb[6]
                _first = false
            end
            if _bb[5] < min_z
                min_z = _bb[5]
            end
            if _bb[5] > max_z
                max_z = _bb[6]
            end
        end
    end

    if min_z == max_z == 0
        return (min_x, max_x, min_y, max_y)
    else
        return (min_x, max_x, min_y, max_y, min_z, max_z)
    end
end

function pseudo_center(dataset::T) where {T<:AbstractVTKData}
    _bb = bb(dataset)
    return ntuple(i -> (_bb[2i-1] + _bb[2i])/2, Val(length(_bb)÷2))
end