module CTImages

export rescale!, rescale, rotate
export polar2cart
export AbstractCTImage, CTImage, CTSinogram, CTTomogram
export CTImageOrTomog, CTImageMat, CTSinogMat, CTTomogMat
export ctimage, ctsinogram, cttomogram

using IntervalSets, SimpleTraits
import ..Monads: mjoin
using ..Utils: linspace, half, _compute_radius, _compute_angle, _wrap_angle
import ..Utils: _atype
using ..Applicative, ..Monads, ..Interpolation
using ..Geometry:
    AbstractParallelBeamGeometry, AbstractFanBeamGeometry, ParallelBeamGeometry
import Base: size, convert, similar, eltype, getindex, setindex!, IndexStyle
using Base: @propagate_inbounds

function ctimage end
function ctsinogram end
function cttomogram end

abstract type AbstractCTImage{T<:Number} <: AbstractMatrix{T} end

@traitimpl Monad{AbstractCTImage}
mjoin(img::AbstractCTImage) = img.data

# struct CTImageStyle <: Broadcast.AbstractArrayStyle{2} end
# Base.BroadcastStyle(::Type{<:AbstractCTImage}) = CTImageStyle()
# CTImageStyle(::Val{2}) = CTImageStyle()
# CTImageStyle(::Val{N}) where N = Broadcast.DefaultArrayStyle{N}()

_atype(img::M) where {M <: AbstractCTImage} = _atype(M)
eltype(::Type{M}) where {M<:AbstractCTImage{T}} where T = T
size(img::AbstractCTImage) = mbind(size, img)
size(img::AbstractCTImage, dim::Int) = size(mjoin(img), dim)
@propagate_inbounds function getindex(img::AbstractCTImage, i::Int)
    @boundscheck checkbounds(img, i)
    @inbounds getindex(mjoin(img), i)
end
@propagate_inbounds function getindex(
    img::AbstractCTImage, I::Vararg{Int,2}
)
    @boundscheck checkbounds(img, I...)
    @inbounds getindex(mjoin(img), I...)
end
@propagate_inbounds function setindex!(img::AbstractCTImage, v, i::Int)
    @boundscheck checkbounds(img, i)
    @inbounds setindex!(mjoin(img), v, i)
end
@propagate_inbounds function setindex!(
    img::AbstractCTImage, v, I::Vararg{Int,2}
)
    @boundscheck checkbounds(img, I...)
    @inbounds setindex!(mjoin(img), v, I...)
end

const ctnames = (image = :CTImage, sinog = :CTSinogram, tomog = :CTTomogram)
const ctfn = (image = :ctimage, sinog = :ctsinogram, tomog = :cttomogram)

for nm in ctnames
    @eval begin
        struct $nm{T<:Number,M<:AbstractMatrix{T}} <: AbstractCTImage{T}
            data::M
        end
        @inline _atype(::Type{$nm{T,M}}) where {T,M} = M
        IndexStyle(::Type{T}) where T<:$nm = IndexStyle(_atype(T))
        @inline function $nm{T,M}(
            ::UndefInitializer,
            dims::Vararg{Union{Integer,AbstractUnitRange},2}
        ) where {T,M}
            $nm(M(undef, dims...))
        end
        @inline $nm{T,M}(img::$nm) where {T,M} = $nm{T,M}(mjoin(img))
        @inline $nm{T}(img::M) where {T,M<:AbstractMatrix{T}} = $nm{T,M}(img)
        @inline $nm(img::$nm) = img
        @inline convert(::Type{M}, img::AbstractMatrix) where {M<:$nm} =
            mreturn(M, img)
        @inline convert(::Type{M}, img::$nm) where {M<:$nm} =
            mreturn($nm, convert(_atype(M), mjoin(img)))
        @inline function similar(img::M) where {M<:$nm}
            mreturn(M, similar(mjoin(img)))
        end
        @inline function similar(
            img::M,
            ::Type{S},
            dims::Tuple{Vararg{Int,2}}
        ) where {S,M<:$nm}
            mreturn(M, similar(mjoin(img), S, dims))
        end
    end
end

CTImage(::Union{CTSinogram,CTTomogram}) =
    @error "Cannot construct a CT image from a sinogram or a tomogram"
CTSinogram(::Union{CTImage,CTTomogram}) =
    @error "Cannot construct a CT sinogram from an image or a tomogram"
CTTomogram(::Union{CTImage,CTSinogram}) =
    @error "Cannot construct a CT tomogram from an image or a sinogram"

const CTImageOrTomog{T,M} = Union{CTImage{T,M},CTTomogram{T,M}}
const CTImageMat{T} = CTImage{T,Matrix{T}}
const CTSinogMat{T} = CTSinogram{T,Matrix{T}}
const CTTomogMat{T} = CTTomogram{T,Matrix{T}}

"""
    rotate(mat::AbstractMatrix, α::Real; <keyword arguments>)

Rotate matrix `mat` about the center of angle `α` given in degrees.
If `rows` and `cols` are not given, the rotated matrix has the same
dimensions of the original matrix.

# Arguments
- `mat`: matrix to rotate.
- `α`: angle in degrees.
- `rows=nothing`: number of rows of the rotated matrix.
- `cols=nothing`: number of columns of the rotated matrix.
- `interpolation`: interpolation strategy. By default is
    `BilinearInterpolation`.
"""
function rotate(
    mat::AbstractMatrix{T},
    α::Real;
    rows::Optional{Integer} = nothing,
    cols::Optional{Integer} = nothing,
    background::Optional{Real} = nothing,
    interpolation::Optional{Interp} = nothing,
) where {T <: Number,Interp <: AbstractInterp2DOrNone}
    orows, ocols = size(mat)
    rows = maybe(orows, rows)
    cols = maybe(ocols, cols)
    sϕ, cϕ = sincos(deg2rad(α))
    interpolation = maybe(interpolate, interpolation)
    interp = interpolation(mat)
    x₀::T = (cols + 1) / 2
    y₀::T = (rows + 1) / 2
    x′₀::T = (ocols + 1) / 2
    y′₀::T = (orows + 1) / 2
    z::T = maybe(zero(T), background)
    rmat = similar(mat, rows, cols)
    fill!(rmat, z)
    for ix ∈ 1:rows, iy ∈ 1:cols
        x = T(ix) - x₀
        y = T(iy) - y₀
        x′ = x * cϕ + y * sϕ + x′₀
        y′ = y * cϕ - x * sϕ + y′₀
        if x′ ∈ 1..ocols && y′ ∈ 1..orows
            rmat[iy, ix] = interp(y′, x′)
        end
    end
    rmat
end

"""
    rescale!(x::AbstractArray;
        interval=nothing, calibration=nothing, window=nothing)

Rescale array x to the interval specified by `interval`.

If `calibration` is not `nothing`, then rescaling is done with
reference to the values given by `calibration`. In other words,
minimum and maximum are assumed to be the values specified by
`calibration`.

See also: [`rescale`](@ref)
"""
function rescale!(
    img::AbstractArray{T};
    interval::ClosedInterval = zero(T)..one(T),
    calibration::Optional{ClosedInterval{U}} = nothing,
    window::Optional{ClosedInterval{W}} = nothing,
) where {T <: Number,U <: Number,W <: Number}
    if isnothing(calibration)
        calibration = ClosedInterval(extrema(img)...)
    end
    a, b = T.(endpoints(interval))
    m, M = T.(endpoints(calibration))
    if m == M
        @warn "Cannot calibrate image as calibration values ($calibration) are equal, image unchanged"
        return img
    end
    f = (b - a) / (M - m)
    if m != zero(T)
        img .-= m
    end
    img .*= f
    if a != zero(T)
        img .+= a
    end
    isnothing(window) && return img
    a, b = T.(endpoints(window))
    map!(img, img) do x
        x < a && return a
        x > b && return b
        return x
    end
    img
end

"""
    rescale(x::AbstractArray, slope::Number, intercept::Number; window)

Linear rescaling of `x` as `x * slope + intercept`.

See also: [`rescale!`](@ref)
"""
function rescale(
    image::AbstractArray{T},
    slope::Number,
    intercept::Number;
    window::Optional{ClosedInterval{U}} = nothing,
) where {T <: Number,U <: Number}
    res = @. image * slope + intercept
    isnothing(window) && return res
    a, b = T.(endpoints(window))
    map!(res, res) do x
        x < a && return a
        x > b && return b
        return x
    end
end

"""
    rescale!(x::AbstractArray, slope::Number, intercept::Number)

In place linear rescaling of `x`.

See also: [`rescale`](@ref)
"""
function rescale!(
    img::AbstractArray{T},
    slope::Number,
    intercept::Number;
    window::Optional{ClosedInterval{U}} = nothing,
) where {T <: Number,U <: Number}
    @. img = slope * img + intercept
    isnothing(window) && return img
    a, b = T.(endpoints(window))
    map!(img, img) do x
        x < a && return a
        x > b && return b
        return x
    end
    img
end

"""
    rescale(x::AbstractArray;
        interval=nothing, calibration=nothing, window=nothing)

Rescale array x to the interval specified by `interval`.

If `calibration` is not `nothing`, then rescaling is done with
reference to the values given by `calibration`. In other words,
minimum and maximum are assumed to be the values specified by
`calibration`.

See also: [`rescale!`](@ref)
"""
function rescale(
    x::AbstractArray;
    interval = 0..1,
    calibration = nothing,
    window = nothing,
)
    rescale!(deepcopy(x); interval, calibration, window)
end

function polar2cart(
    mp::M,
    xs::AbstractVector,
    ys::AbstractVector;
    ν::Real = 1,
    scale::Optional{Real} = nothing,
    diag::Bool = false,
    background::Optional{Real} = nothing,
    transposed::Bool = false,
    interpolation::Optional{Interp} = nothing,
)::M where {
    M <: AbstractMatrix{T},
    Interp <: AbstractInterp2DOrNone,
} where {T <: Real}
    if transposed
        nr, nθ = size(mp)
        mp′ = similar(mp, nθ, nr)
        permutedims!(mp′, mp, (2,1))
        return polar2cart(mp′, xs, ys; background, interpolation)
    end
    ν = maybe(ν, scale)
    @assert ν > 0
    nθ, nr = size(mp)
    x₀, y₀ = half(xs), half(ys)
    κ::T = min(x₀, y₀)
    if diag
        ν *= hypot(x₀, y₀) / κ
    end
    δri::T = (nr-1) / (κ * ν)
    δθi::T = nθ / 2π
    xs′ = xs * δri
    ys′ = ys * δri
    nt1 = nθ + 1
    nthalf = nθ + 1//2
    interpolation = maybe(interpolate, interpolation)
    interp = interpolation(mp)
    rows, cols = length(ys), length(xs)
    z::T = maybe(zero(T), background)
    mc = similar(mp, rows, cols)
    fill!(mc, z)
    Threads.@threads for ix ∈ axes(mc, 2)
        @inbounds @fastmath @simd for iy ∈ axes(mc, 1)
            x::T = xs′[ix]
            y::T = ys′[iy]
            r::T = _compute_radius(x, y)
            θ::T = _compute_angle(x, y, δθi)
            if 1 <= r <= nr && 1 <= θ <= nt1
                if θ >= nthalf
                    θ = one(T)
                elseif θ > nθ
                    θ = nθ
                end
                mc[iy,ix] = interp(θ, r)
            end
        end
    end
    mc
end

@inline function polar2cart(
    mp::AbstractMatrix{T};
    rows::Optional{Integer} = nothing,
    cols::Optional{Integer} = nothing,
    transposed::Bool = false,
    kwargs...
) where {T <: Real,Interp <: AbstractInterp2DOrNone}
    mp = transposed ? permutedims(mp) : mp
    rows = isnothing(rows) ? maybe(2 * size(mp, 2), cols) : rows
    cols = maybe(rows, cols)
    y₀, x₀ = sincos(atan(rows, cols))
    xs = linspace(T, -x₀..x₀, cols)
    ys = linspace(T, -y₀..y₀, rows)
    polar2cart(mp, xs, ys; kwargs...)
end

function polar2cart(
    image::AbstractMatrix,
    geometry::AbstractParallelBeamGeometry;
    kwargs...
)
    polar2cart(image; geometry.rows, geometry.cols, kwargs...)
end

function polar2cart(
    image::AbstractMatrix,
    geometry::AbstractFanBeamGeometry;
    kwargs...
)
    polar2cart(image, ParallelBeamGeometry(geometry); kwargs...)
end

polar2cart(; kwargs...) = x -> polar2cart(x; kwargs...)
polar2cart(g::AbstractParallelBeamGeometry; kwargs...) =
    x -> polar2cart(x, g; kwargs...)

end # module


using KernelAbstractions: NoneEvent
using OffsetArrays: OffsetArray
using CUDA

using Oceananigans.Operators: Δzᵃᵃᶜ
using Oceananigans.BoundaryConditions: left_gradient, right_gradient, linearly_extrapolate, FBC, VBC, GBC
using Oceananigans.BoundaryConditions: fill_bottom_halo!, fill_top_halo!, apply_z_bottom_bc!, apply_z_top_bc!
using Oceananigans.Grids: Flat, Bounded
using Oceananigans.Coriolis: AbstractRotation
using Oceananigans.Architectures: device_event
using Oceananigans.TurbulenceClosures: AbstractTurbulenceClosure
using Oceananigans.TurbulenceClosures.CATKEVerticalDiffusivities: _top_tke_flux, CATKEVDArray

import Oceananigans.Utils: launch!
import Oceananigans.Grids: validate_size, validate_halo
import Oceananigans.BoundaryConditions: fill_halo_regions!
import Oceananigans.TurbulenceClosures: time_discretization, calculate_diffusivities!
import Oceananigans.TurbulenceClosures: ∂ⱼ_τ₁ⱼ, ∂ⱼ_τ₂ⱼ, ∂ⱼ_τ₃ⱼ, ∇_dot_qᶜ
import Oceananigans.TurbulenceClosures.CATKEVerticalDiffusivities: top_tke_flux
import Oceananigans.Coriolis: x_f_cross_U, y_f_cross_U, z_f_cross_U

#####
##### Implements a "single column model mode" for HydrostaticFreeSurfaceModel
#####

const SingleColumnGrid = AbstractGrid{<:AbstractFloat, <:Flat, <:Flat, <:Bounded}

#####
##### Model constructor utils
#####

PressureField(::SingleColumnGrid) = (; pHY′ = nothing)
FreeSurface(free_surface::ExplicitFreeSurface{Nothing}, velocities, arch, ::SingleColumnGrid) = nothing
FreeSurface(free_surface::ImplicitFreeSurface{Nothing}, velocities, arch, ::SingleColumnGrid) = nothing

validate_momentum_advection(momentum_advection, ::SingleColumnGrid) = nothing
validate_tracer_advection(tracer_advection::AbstractAdvectionScheme, ::SingleColumnGrid) = nothing, NamedTuple()
validate_tracer_advection(tracer_advection::Nothing, ::SingleColumnGrid) = nothing, NamedTuple()

#####
##### Time-step optimizations
#####

calculate_free_surface_tendency!(arch, ::SingleColumnGrid, args...) = NoneEvent()

# Fast state update and halo filling

function update_state!(model::HydrostaticFreeSurfaceModel, grid::SingleColumnGrid)

    fill_halo_regions!(prognostic_fields(model), model.architecture, model.clock, fields(model))

    compute_auxiliary_fields!(model.auxiliary_fields)

    # Calculate diffusivities
    calculate_diffusivities!(model.diffusivity_fields, model.closure, model)

    fill_halo_regions!(model.diffusivity_fields,
                       model.architecture,
                       model.clock,
                       fields(model))

    return nothing
end

const ClosureArray = AbstractArray{<:AbstractTurbulenceClosure}

@inline function ∂ⱼ_τ₁ⱼ(i, j, k, grid::SingleColumnGrid, closure_array::ClosureArray, args...)
    @inbounds closure = closure_array[i, j]
    return ∂ⱼ_τ₁ⱼ(i, j, k, grid, closure, args...)
end

@inline function ∂ⱼ_τ₂ⱼ(i, j, k, grid::SingleColumnGrid, closure_array::ClosureArray, args...)
    @inbounds closure = closure_array[i, j]
    return ∂ⱼ_τ₂ⱼ(i, j, k, grid, closure, args...)
end

@inline function ∇_dot_qᶜ(i, j, k, grid::SingleColumnGrid, closure_array::ClosureArray, c, tracer_index, args...)
    @inbounds closure = closure_array[i, j]
    return ∇_dot_qᶜ(i, j, k, grid, closure, c, tracer_index, args...)
end

struct ColumnEnsembleSize{C<:Tuple{Int, Int}}
    ensemble :: C
    Nz :: Int
    Hz :: Int
end

ColumnEnsembleSize(; Nz, ensemble=(0, 0), Hz=1) = ColumnEnsembleSize(ensemble, Nz, Hz)

validate_size(TX, TY, TZ, e::ColumnEnsembleSize) = tuple(e.ensemble[1], e.ensemble[2], e.Nz)
validate_halo(TX, TY, TZ, e::ColumnEnsembleSize) = tuple(0, 0, e.Hz)

@inline function time_discretization(closure_array::AbstractArray)
    first_closure = CUDA.@allowscalar first(closure_array) # assumes all closures have same time-discretization
    return time_discretization(first_closure)
end

#####
##### CATKEVerticalDiffusivity helpers
#####

@inline tracer_tendency_kernel_function(model::HydrostaticFreeSurfaceModel, closure::CATKEVDArray, ::Val{:e}) =
    hydrostatic_turbulent_kinetic_energy_tendency

""" Compute the flux of TKE through the surface / top boundary. """
@inline function top_tke_flux(i, j, grid::SingleColumnGrid, clock, fields, parameters, closure_array::CATKEVDArray, buoyancy)
    top_tracer_bcs = parameters.top_tracer_boundary_conditions
    top_velocity_bcs = parameters.top_velocity_boundary_conditions
    @inbounds closure = closure_array[i, j]

    return _top_tke_flux(i, j, grid, closure.surface_TKE_flux, closure,
                         buoyancy, fields, top_tracer_bcs, top_velocity_bcs, clock)
end

@inline function hydrostatic_turbulent_kinetic_energy_tendency(i, j, k, grid::SingleColumnGrid,
                                                               val_tracer_index::Val{tracer_index},
                                                               advection,
                                                               closure_array::CATKEVDArray, args...) where tracer_index

    @inbounds closure = closure_array[i, j]
    return hydrostatic_turbulent_kinetic_energy_tendency(i, j, k, grid, val_tracer_index, advection, closure, args...)
end

#####
##### Arrays of Coriolises
#####

const CoriolisMatrix = AbstractMatrix{<:AbstractRotation}

@inline function x_f_cross_U(i, j, k, grid::SingleColumnGrid, coriolis_array::CoriolisMatrix, U)
    @inbounds coriolis = coriolis_array[i, j]
    return x_f_cross_U(i, j, k, grid, coriolis, U)
end

@inline function y_f_cross_U(i, j, k, grid::SingleColumnGrid, coriolis_array::CoriolisMatrix, U)
    @inbounds coriolis = coriolis_array[i, j]
    return y_f_cross_U(i, j, k, grid, coriolis, U)
end

@inline function z_f_cross_U(i, j, k, grid::SingleColumnGrid, coriolis_array::CoriolisMatrix, U)
    @inbounds coriolis = coriolis_array[i, j]
    return z_f_cross_U(i, j, k, grid, coriolis, U)
end


# modified by N Laohakunakorn
# University of Edinburgh 2020

using Statistics
# ----------------------------------------------------------------------------------- #
# Copyright (c) 2017 Varnerlab
# School of Chemical Engineering Purdue University
# W. Lafayette IN 46907 USA

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# ----------------------------------------------------------------------------------- #
function Control(t,x,rate_vector,data_dictionary)
# ---------------------------------------------------------------------- #
# Control.jl was generated using the Kwatee code generation system.
# Username: nicholas
# Type: CFPS-JULIA
# Version: 1.0
# Generation timestamp: 03-24-2017 13:01:36
#
# Arguments:
# t  - current time
# x  - state vector
# rate_vector - vector of reaction rates
# data_dictionary  - Data dictionary instance (holds model parameters)
# ---------------------------------------------------------------------- #

# Set a default value for the allosteric control variables -
number_of_reactions = length(rate_vector);
control_vector = ones(number_of_reactions);
control_parameter_array = data_dictionary["CONTROL_PARAMETER_ARRAY"];

# Alias the species vector -
M_g6p_c = x[1];
M_f6p_c = x[2];
M_fdp_c = x[3];
M_dhap_c = x[4];
M_g3p_c = x[5];
M_13dpg_c = x[6];
M_3pg_c = x[7];
M_2pg_c = x[8];
M_oaa_c = x[9];
M_coa_c = x[10];
M_accoa_c = x[11];
M_6pgl_c = x[12];
M_6pgc_c = x[13];
M_ru5p_D_c = x[14];
M_xu5p_D_c = x[15];
M_r5p_c = x[16];
M_s7p_c = x[17];
M_e4p_c = x[18];
M_2ddg6p_c = x[19];
M_cit_c = x[20];
M_icit_c = x[21];
M_akg_c = x[22];
M_succoa_c = x[23];
M_q8_c = x[24];
M_fum_c = x[25];
M_q8h2_c = x[26];
M_mql8_c = x[27];
M_mqn8_c = x[28];
M_h_e = x[29];
M_ppi_c = x[30];
M_glx_c = x[31];
M_actp_c = x[32];
M_etoh_c = x[33];
M_for_c = x[34];
M_nh3_c = x[35];
M_arg_L_c = x[36];
M_h2s_c = x[37];
M_thf_c = x[38];
M_mlthf_c = x[39];
M_aicar_c = x[40];
M_5mthf_c = x[41];
M_chor_c = x[42];
M_h2o2_c = x[43];
M_mglx_c = x[44];
M_prop_c = x[45];
M_indole_c = x[46];
M_cadav_c = x[47];
M_gaba_c = x[48];
M_glycoA_c = x[49];
M_78mdp_c = x[50];
M_4adochor_c = x[51];
M_4abz_c = x[52];
M_78dhf_c = x[53];
M_dhf_c = x[54];
M_methf_c = x[55];
M_10fthf_c = x[56];
M_prpp_c = x[57];
M_hco3_c = x[58];
M_clasp_c = x[59];
M_or_c = x[60];
M_omp_c = x[61];
M_5pbdra = x[62];
M_gar_c = x[63];
M_fgar_c = x[64];
M_fgam_c = x[65];
M_air_c = x[66];
M_cair_c = x[67];
M_saicar_c = x[68];
M_faicar_c = x[69];
M_imp_c = x[70];
M_xmp_c = x[71];
GENE_CAT = x[72];
RNAP = x[73];
OPEN_GENE_CAT = x[74];
mRNA_CAT = x[75];
RIBOSOME = x[76];
RIBOSOME_START_CAT = x[77];
M_ala_L_c_tRNA = x[78];
M_arg_L_c_tRNA = x[79];
M_asn_L_c_tRNA = x[80];
M_asp_L_c_tRNA = x[81];
M_cys_L_c_tRNA = x[82];
M_glu_L_c_tRNA = x[83];
M_gln_L_c_tRNA = x[84];
M_gly_L_c_tRNA = x[85];
M_his_L_c_tRNA = x[86];
M_ile_L_c_tRNA = x[87];
M_leu_L_c_tRNA = x[88];
M_lys_L_c_tRNA = x[89];
M_met_L_c_tRNA = x[90];
M_phe_L_c_tRNA = x[91];
M_pro_L_c_tRNA = x[92];
M_ser_L_c_tRNA = x[93];
M_thr_L_c_tRNA = x[94];
M_trp_L_c_tRNA = x[95];
M_tyr_L_c_tRNA = x[96];
M_val_L_c_tRNA = x[97];
PROTEIN_CAT = x[98];
tRNA = x[99];
M_glc_D_c = x[100];
M_pep_c = x[101];
M_pyr_c = x[102];
M_ac_c = x[103];
M_lac_D_c = x[104];
M_mal_L_c = x[105];
M_atp_c = x[106];
M_adp_c = x[107];
M_amp_c = x[108];
M_gtp_c = x[109];
M_gdp_c = x[110];
M_gmp_c = x[111];
M_utp_c = x[112];
M_udp_c = x[113];
M_ump_c = x[114];
M_ctp_c = x[115];
M_cdp_c = x[116];
M_cmp_c = x[117];
M_succ_c = x[118];
M_asp_L_c = x[119];
M_gly_L_c = x[120];
M_ile_L_c = x[121];
M_asn_L_c = x[122];
M_cys_L_c = x[123];
M_lys_L_c = x[124];
M_his_L_c = x[125];
M_ala_L_c = x[126];
M_phe_L_c = x[127];
M_pro_L_c = x[128];
M_ser_L_c = x[129];
M_thr_L_c = x[130];
M_trp_L_c = x[131];
M_tyr_L_c = x[132];
M_val_L_c = x[133];
M_met_L_c = x[134];
M_leu_L_c = x[135];
M_glu_L_c = x[136];
M_gln_L_c = x[137];
M_o2_c = x[138];
M_co2_c = x[139];
M_pi_c = x[140];
M_nh4_c = x[141];
M_so4_c = x[142];
M_h_c = x[143];
M_h2o_c = x[144];
M_nad_c = x[145];
M_nadh_c = x[146];
M_nadp_c = x[147];
M_nadph_c = x[148];
E_R_glk_atp = x[149];
E_R_pgi = x[150];
E_R_pgi_R = x[151];
E_R_pfk = x[152];
E_R_fdp = x[153];
E_R_fbaA = x[154];
E_R_fbaA_R = x[155];
E_R_tpiA = x[156];
E_R_tpiA_R = x[157];
E_R_gapA = x[158];
E_R_gapA_R = x[159];
E_R_pgk = x[160];
E_R_pgk_R = x[161];
E_R_gpm = x[162];
E_R_gpm_R = x[163];
E_R_eno = x[164];
E_R_eno_R = x[165];
E_R_pyk = x[166];
E_R_pck = x[167];
E_R_ppc = x[168];
E_R_pdh = x[169];
E_R_pps = x[170];
E_R_zwf = x[171];
E_R_zwf_R = x[172];
E_R_pgl = x[173];
E_R_gnd = x[174];
E_R_rpe = x[175];
E_R_rpe_R = x[176];
E_R_rpi = x[177];
E_R_rpi_R = x[178];
E_R_talAB = x[179];
E_R_talAB_R = x[180];
E_R_tkt1 = x[181];
E_R_tkt1_R = x[182];
E_R_tkt2 = x[183];
E_R_tkt2_R = x[184];
E_R_edd = x[185];
E_R_eda = x[186];
E_R_gltA = x[187];
E_R_acn = x[188];
E_R_acn_R = x[189];
E_R_icd = x[190];
E_R_icd_R = x[191];
E_R_sucAB = x[192];
E_R_sucCD = x[193];
E_R_sdh = x[194];
E_R_frd = x[195];
E_R_fum = x[196];
E_R_fum_R = x[197];
E_R_mdh = x[198];
E_R_mdh_R = x[199];
E_R_cyd = x[200];
E_R_cyo = x[201];
E_R_app = x[202];
E_R_atp = x[203];
E_R_nuo = x[204];
E_R_pnt1 = x[205];
E_R_pnt2 = x[206];
E_R_ndh1 = x[207];
E_R_ndh2 = x[208];
E_R_ppa = x[209];
E_R_aceA = x[210];
E_R_aceB = x[211];
E_R_maeA = x[212];
E_R_maeB = x[213];
E_R_pta = x[214];
E_R_pta_R = x[215];
E_R_ackA = x[216];
E_R_ackA_R = x[217];
E_R_acs = x[218];
E_R_adhE = x[219];
E_R_adhE_R = x[220];
E_R_ldh = x[221];
E_R_ldh_R = x[222];
E_R_pflAB = x[223];
E_R_alaAC = x[224];
E_R_alaAC_R = x[225];
E_R_arg = x[226];
E_R_aspC = x[227];
E_R_asnB = x[228];
E_R_asnA = x[229];
E_R_cysEMK = x[230];
E_R_gltBD = x[231];
E_R_gdhA = x[232];
E_R_gdhA_R = x[233];
E_R_glnA = x[234];
E_R_glyA = x[235];
E_R_his = x[236];
E_R_ile = x[237];
E_R_leu = x[238];
E_R_lys = x[239];
E_R_met = x[240];
E_R_phe = x[241];
E_R_pro = x[242];
E_R_serABC = x[243];
E_R_thr = x[244];
E_R_trp = x[245];
E_R_tyr = x[246];
E_R_val = x[247];
E_R_arg_deg = x[248];
E_R_asp_deg = x[249];
E_R_asn_deg = x[250];
E_R_gly_deg = x[251];
E_R_mglx_deg = x[252];
E_R_ser_deg = x[253];
E_R_pro_deg = x[254];
E_R_thr_deg1 = x[255];
E_R_thr_deg2 = x[256];
E_R_thr_deg3 = x[257];
E_R_trp_deg = x[258];
E_R_cys_deg = x[259];
E_R_lys_deg = x[260];
E_R_gln_deg = x[261];
E_R_glu_deg = x[262];
E_R_gaba_deg1 = x[263];
E_R_gaba_deg2 = x[264];
E_R_chor = x[265];
E_R_fol_e = x[266];
E_R_fol_1 = x[267];
E_R_fol_2a = x[268];
E_R_fol_2b = x[269];
E_R_fol_3 = x[270];
E_R_fol_4 = x[271];
E_R_gly_fol = x[272];
E_R_gly_fol_R = x[273];
E_R_mthfd = x[274];
E_R_mthfd_R = x[275];
E_R_mthfc = x[276];
E_R_mthfc_R = x[277];
E_R_mthfr2a = x[278];
E_R_mthfr2b = x[279];
E_R_prpp_syn = x[280];
E_R_or_syn_1 = x[281];
E_R_or_syn_2 = x[282];
E_R_omp_syn = x[283];
E_R_ump_syn = x[284];
E_R_ctp_1 = x[285];
E_R_ctp_2 = x[286];
E_R_A_syn_1 = x[287];
E_R_A_syn_2 = x[288];
E_R_A_syn_3 = x[289];
E_R_A_syn_4 = x[290];
E_R_A_syn_5 = x[291];
E_R_A_syn_6 = x[292];
E_R_A_syn_7 = x[293];
E_R_A_syn_8 = x[294];
E_R_A_syn_9 = x[295];
E_R_A_syn_10 = x[296];
E_R_A_syn_12 = x[297];
E_R_xmp_syn = x[298];
E_R_gmp_syn = x[299];
E_R_atp_amp = x[300];
E_R_utp_ump = x[301];
E_R_ctp_cmp = x[302];
E_R_gtp_gmp = x[303];
E_R_atp_adp = x[304];
E_R_utp_adp = x[305];
E_R_ctp_adp = x[306];
E_R_gtp_adp = x[307];
E_R_udp_utp = x[308];
E_R_cdp_ctp = x[309];
E_R_gdp_gtp = x[310];
E_R_atp_ump = x[311];
E_R_atp_cmp = x[312];
E_R_atp_gmp = x[313];
E_R_adk_atp = x[314];
E_Import_o2 = x[315];
E_Import_co2 = x[316];
E_Import_pi = x[317];
E_Import_nh4 = x[318];
E_Import_so4 = x[319];
E_Import_h2o = x[320];
E_Export_o2 = x[321];
E_Export_co2 = x[322];
E_Export_pi = x[323];
E_Export_nh4 = x[324];
E_Export_so4 = x[325];
E_Export_h2o = x[326];
E_Proton_gradient = x[327];
E_transcriptional_initiation_CAT = x[328];
E_transcription_CAT = x[329];
E_mRNA_degradation_CAT = x[330];
E_translation_initiation_CAT = x[331];
E_translation_CAT = x[332];
E_tRNA_charging_M_ala_L_c_CAT = x[333];
E_tRNA_charging_M_arg_L_c_CAT = x[334];
E_tRNA_charging_M_asn_L_c_CAT = x[335];
E_tRNA_charging_M_asp_L_c_CAT = x[336];
E_tRNA_charging_M_cys_L_c_CAT = x[337];
E_tRNA_charging_M_glu_L_c_CAT = x[338];
E_tRNA_charging_M_gln_L_c_CAT = x[339];
E_tRNA_charging_M_gly_L_c_CAT = x[340];
E_tRNA_charging_M_his_L_c_CAT = x[341];
E_tRNA_charging_M_ile_L_c_CAT = x[342];
E_tRNA_charging_M_leu_L_c_CAT = x[343];
E_tRNA_charging_M_lys_L_c_CAT = x[344];
E_tRNA_charging_M_met_L_c_CAT = x[345];
E_tRNA_charging_M_phe_L_c_CAT = x[346];
E_tRNA_charging_M_pro_L_c_CAT = x[347];
E_tRNA_charging_M_ser_L_c_CAT = x[348];
E_tRNA_charging_M_thr_L_c_CAT = x[349];
E_tRNA_charging_M_trp_L_c_CAT = x[350];
E_tRNA_charging_M_tyr_L_c_CAT = x[351];
E_tRNA_charging_M_val_L_c_CAT = x[352];

transfer_function_vector = Float64[];
# type: inhibition actor: M_pep_c target: R_pfk
push!(transfer_function_vector,1.0 - control_parameter_array[1,1]*M_pep_c^control_parameter_array[1,2]/(1+control_parameter_array[1,1]*M_pep_c^control_parameter_array[1,2]));
control_vector[4] = mean(transfer_function_vector);
transfer_function_vector = 0;

transfer_function_vector = Float64[];
# type: activation actor: M_pep_c target: R_fdp
push!(transfer_function_vector,control_parameter_array[2,1]*M_pep_c^control_parameter_array[2,2]/(1+control_parameter_array[2,1]*M_pep_c^control_parameter_array[2,2]));
control_vector[5] = mean(transfer_function_vector);
transfer_function_vector = 0;

transfer_function_vector = Float64[];
# type: activation actor: M_fdp_c target: R_pyk
push!(transfer_function_vector,control_parameter_array[3,1]*M_fdp_c^control_parameter_array[3,2]/(1+control_parameter_array[3,1]*M_fdp_c^control_parameter_array[3,2]));
control_vector[18] = mean(transfer_function_vector);
transfer_function_vector = 0;

transfer_function_vector = Float64[];
# type: inhibition actor: M_pep_c target: R_pck
push!(transfer_function_vector,1.0 - control_parameter_array[4,1]*M_pep_c^control_parameter_array[4,2]/(1+control_parameter_array[4,1]*M_pep_c^control_parameter_array[4,2]));
control_vector[19] = mean(transfer_function_vector);
transfer_function_vector = 0;

transfer_function_vector = Float64[];
# type: activation actor: M_fdp_c target: R_ppc
push!(transfer_function_vector,control_parameter_array[5,1]*M_fdp_c^control_parameter_array[5,2]/(1+control_parameter_array[5,1]*M_fdp_c^control_parameter_array[5,2]));
control_vector[20] = mean(transfer_function_vector);
transfer_function_vector = 0;

transfer_function_vector = Float64[];
# type: inhibition actor: M_pyr_c target: R_pdh
push!(transfer_function_vector,1.0 - control_parameter_array[6,1]*M_pyr_c^control_parameter_array[6,2]/(1+control_parameter_array[6,1]*M_pyr_c^control_parameter_array[6,2]));
control_vector[21] = mean(transfer_function_vector);
transfer_function_vector = 0;

transfer_function_vector = Float64[];
# type: inhibition actor: M_pep_c target: R_pps
push!(transfer_function_vector,1.0 - control_parameter_array[7,1]*M_pep_c^control_parameter_array[7,2]/(1+control_parameter_array[7,1]*M_pep_c^control_parameter_array[7,2]));
control_vector[22] = mean(transfer_function_vector);
transfer_function_vector = 0;

transfer_function_vector = Float64[];
# type: inhibition actor: M_akg_c target: R_gltA
push!(transfer_function_vector,1.0 - control_parameter_array[8,1]*M_akg_c^control_parameter_array[8,2]/(1+control_parameter_array[8,1]*M_akg_c^control_parameter_array[8,2]));
control_vector[39] = mean(transfer_function_vector);
transfer_function_vector = 0;

transfer_function_vector = Float64[];
# type: inhibition actor: M_pep_c target: R_icd
push!(transfer_function_vector,1.0 - control_parameter_array[9,1]*M_pep_c^control_parameter_array[9,2]/(1+control_parameter_array[9,1]*M_pep_c^control_parameter_array[9,2]));
control_vector[42] = mean(transfer_function_vector);
transfer_function_vector = 0;

transfer_function_vector = Float64[];
# type: inhibition actor: M_akg_c target: R_aceA
push!(transfer_function_vector,1.0 - control_parameter_array[10,1]*M_akg_c^control_parameter_array[10,2]/(1+control_parameter_array[10,1]*M_akg_c^control_parameter_array[10,2]));
# type: inhibition actor: M_pep_c target: R_aceA
push!(transfer_function_vector,1.0 - control_parameter_array[11,1]*M_pep_c^control_parameter_array[11,2]/(1+control_parameter_array[11,1]*M_pep_c^control_parameter_array[11,2]));
# type: inhibition actor: M_3pg_c target: R_aceA
push!(transfer_function_vector,1.0 - control_parameter_array[12,1]*M_3pg_c^control_parameter_array[12,2]/(1+control_parameter_array[12,1]*M_3pg_c^control_parameter_array[12,2]));
control_vector[62] = mean(transfer_function_vector);
transfer_function_vector = 0;

transfer_function_vector = Float64[];
# type: inhibition actor: M_akg_c target: R_aceB
push!(transfer_function_vector,1.0 - control_parameter_array[13,1]*M_akg_c^control_parameter_array[13,2]/(1+control_parameter_array[13,1]*M_akg_c^control_parameter_array[13,2]));
# type: inhibition actor: M_pep_c target: R_aceB
push!(transfer_function_vector,1.0 - control_parameter_array[14,1]*M_pep_c^control_parameter_array[14,2]/(1+control_parameter_array[14,1]*M_pep_c^control_parameter_array[14,2]));
# type: inhibition actor: M_3pg_c target: R_aceB
push!(transfer_function_vector,1.0 - control_parameter_array[15,1]*M_3pg_c^control_parameter_array[15,2]/(1+control_parameter_array[15,1]*M_3pg_c^control_parameter_array[15,2]));
control_vector[63] = mean(transfer_function_vector);
transfer_function_vector = 0;

transfer_function_vector = Float64[];
# type: inhibition actor: M_accoa_c target: R_maeB
push!(transfer_function_vector,1.0 - control_parameter_array[16,1]*M_accoa_c^control_parameter_array[16,2]/(1+control_parameter_array[16,1]*M_accoa_c^control_parameter_array[16,2]));
control_vector[65] = mean(transfer_function_vector);
transfer_function_vector = 0;

transfer_function_vector = Float64[];
# type: activation actor: M_pyr_c target: R_ldh_R
push!(transfer_function_vector,control_parameter_array[17,1]*M_pyr_c^control_parameter_array[17,2]/(1+control_parameter_array[17,1]*M_pyr_c^control_parameter_array[17,2]));
control_vector[74] = mean(transfer_function_vector);
transfer_function_vector = 0;

# Modify the rate_vector with the control variables -
rate_vector = rate_vector.*control_vector;

# Return the modified rate vector -
return rate_vector;
end


abstract type AbstractLShaped end

StochasticPrograms.num_subproblems(lshaped::AbstractLShaped) = lshaped.num_subproblems
num_cuts(lshaped::AbstractLShaped) = lshaped.data.num_cuts
num_iterations(lshaped::AbstractLShaped) = lshaped.data.iterations
tolerance(lshaped::AbstractLShaped) = lshaped.parameters.τ

# Initialization #
# ======================================================================== #
function initialize!(lshaped::AbstractLShaped)
    # Initialize progress meter
    lshaped.progress.thresh = lshaped.parameters.τ
    # Initialize subproblems
    initialize_subproblems!(lshaped, scenarioproblems(lshaped.structure, 2))
    # Prepare the master optimization problem
    prepare_master!(lshaped)
    # Initialize regularization policy
    initialize_regularization!(lshaped)
    # Finish initialization
    finish_initilization!(lshaped)
    return nothing
end
# ======================================================================== #

# Functions #
# ======================================================================== #
function prepare_master!(lshaped::AbstractLShaped)
    # Check sense first
    sense = MOI.get(lshaped.master, MOI.ObjectiveSense())
    if sense == MOI.FEASIBILITY_SENSE
        lshaped.data.no_objective = true
        # Use min-sense during L-shaped procedure
        MOI.set(lshaped.master, MOI.ObjectiveSense(), MOI.MIN_SENSE)
        F = AffineDecisionFunction{Float64}
        MOI.set(lshaped.master, MOI.ObjectiveFunction{F}(), zero(F))
    else
        # Cache the objective function
        F = MOI.get(lshaped.master, MOI.ObjectiveFunctionType())
        lshaped.data.master_objective = MOI.get(lshaped.master, MOI.ObjectiveFunction{F}())
    end
    # Initialize the required number of master variables. Use
    # MOI.VariableIndex(0) as an undef value
    append!(lshaped.master_variables, fill(MOI.VariableIndex(0), num_thetas(lshaped)))
end

function add_master_variable!(lshaped::AbstractLShaped, index::Integer)
    F = MOI.get(lshaped.master, MOI.ObjectiveFunctionType())
    master_variable = MOI.add_variable(lshaped.master)
    # Set name
    MOI.set(lshaped.master, MOI.VariableName(), master_variable,
            add_subscript("θ", index))
    # Get sense
    sense = MOI.get(lshaped.master, MOI.ObjectiveSense())
    coeff = sense == MOI.MIN_SENSE ? 1.0 : -1.0
    # Add to objective
    MOI.modify(lshaped.master, MOI.ObjectiveFunction{F}(), MOI.ScalarCoefficientChange(master_variable, coeff))
    lshaped.master_variables[index] = master_variable
    return nothing
end

function remove_cut_constraints!(lshaped::AbstractLShaped)
    # Decrease count
    lshaped.data.num_cuts -= length(lshaped.cut_constraints)
    # Remove cuts
    for ci in lshaped.cut_constraints
        if !iszero(ci.value)
            MOI.delete(lshaped.master, ci)
        end
    end
    empty!(lshaped.cut_constraints)
    return nothing
end

function restore_master!(lshaped::AbstractLShaped)
    # Remove cut constraints
    remove_cut_constraints!(lshaped)
    # Remove master variables
    for var in lshaped.master_variables
        if !iszero(var.value)
            MOI.delete(lshaped.master, var)
        end
    end
    empty!(lshaped.master_variables)
    # Remove any regularization terms
    restore_regularized_master!(lshaped)
    if lshaped.data.no_objective
        # Re-set FEASIBILITY_SENSE
        MOI.set(lshaped.master, MOI.ObjectiveSense(), MOI.FEASIBILITY_SENSE)
    else
        # Re-add original objective
        @unpack master_objective = lshaped.data
        F = typeof(master_objective)
        MOI.set(lshaped.master, MOI.ObjectiveFunction{F}(), master_objective)
    end
    return nothing
end

function filter_cuts!(lshaped::AbstractLShaped, list)
    # Nothing to do if constraints do not match
    return nothing
end

function filter_cuts!(lshaped::AbstractLShaped, list::Vector{<:CutConstraint})
    # Filter out the cut constraints
    for cut in optimizer.lshaped.cut_constraints
        i = something(findfirst(isequal(c), list), 0)
        if !iszero(i)
            MOI.deleteat!(list, i)
        end
    end
    return nothing
end

function active_model_objectives(lshaped::AbstractLShaped)
    return map(lshaped.master_variables) do var
        var.value != 0
    end
end

function update_solution!(lshaped::AbstractLShaped)
    ncols = num_decisions(lshaped.structure)
    nb = num_thetas(lshaped)
    lshaped.x .= MOI.get.(lshaped.master, MOI.VariablePrimal(), all_decisions(lshaped.decisions))
    θs = map(lshaped.master_variables) do vi
        if vi.value == 0
            -1e10
        else
            MOI.get(lshaped.master, MOI.VariablePrimal(), vi)
        end
    end
    set_model_objectives(lshaped, θs)
    return nothing
end

function decision(lshaped::AbstractLShaped, index::MOI.VariableIndex)
    i = something(findfirst(i -> i == index, all_decisions(lshaped.decisions)), 0)
    if iszero(i)
        throw(MOI.InvalidIndex(index))
    end
    return decision(lshaped)[i]
end

function evaluate_first_stage(lshaped::AbstractLShaped, x::AbstractVector)
    model = lshaped.master
    # Get objective
    @unpack master_objective = lshaped.data
    # Evaluate objective
    decisions = all_decisions(lshaped.decisions)
    obj_val = MOIU.eval_variables(master_objective) do vi
        if vi in decisions
            # Only evaluate decision
            x[vi.value]
        else
            0.0
        end
    end
    # Return value
    return obj_val
end

function current_objective_value(lshaped::AbstractLShaped)
    # Get sense
    sense = MOI.get(lshaped.master, MOI.ObjectiveSense())
    correction = sense == MOI.MIN_SENSE ? 1.0 : -1.0
    # Return sense-corrected value
    return evaluate_first_stage(lshaped, current_decision(lshaped)) +
        correction * sum(subobjectives(lshaped))
end

function calculate_estimate(lshaped::AbstractLShaped)
    # Get sense
    sense = MOI.get(lshaped.master, MOI.ObjectiveSense())
    correction = sense == MOI.MIN_SENSE ? 1.0 : -1.0
    # Return sense-corrected value
    return evaluate_first_stage(lshaped, lshaped.x) +
        correction * sum(model_objectives(lshaped))
end

function log!(lshaped::AbstractLShaped; optimal = false, status = nothing)
    @unpack Q, θ, iterations = lshaped.data
    @unpack keep, offset, indent = lshaped.parameters
    # Early termination log
    if status != nothing && lshaped.parameters.log
        lshaped.progress.thresh = Inf
        lshaped.progress.printed = true
        val = if status == MOI.INFEASIBLE
            Inf
        elseif status == MOI.DUAL_INFEASIBLE
            -Inf
        else
            0.0
        end
        ProgressMeter.update!(lshaped.progress, val,
                              showvalues = [
                                  ("$(indentstr(indent))Objective", val),
                                  ("$(indentstr(indent))Early termination", status),
                                  ("$(indentstr(indent))Number of cuts", num_cuts(lshaped)),
                                  ("$(indentstr(indent))Iterations", iterations)
                              ], keep = keep, offset = offset)
        return nothing
    end
    # Value update
    push!(lshaped.Q_history, Q)
    push!(lshaped.θ_history, θ)
    lshaped.data.iterations += 1
    # Log
    log_regularization!(lshaped)
    if lshaped.parameters.log
        current_gap = optimal ? 0.0 : gap(lshaped)
        ProgressMeter.update!(lshaped.progress, current_gap,
                              showvalues = [
                                  ("$(indentstr(indent))Objective", objective_value(lshaped)),
                                  ("$(indentstr(indent))Gap", current_gap),
                                  ("$(indentstr(indent))Number of cuts", num_cuts(lshaped)),
                                  ("$(indentstr(indent))Iterations", iterations)
                              ], keep = keep, offset = offset)
    end
    return nothing
end

function log!(lshaped::AbstractLShaped, t::Integer; optimal = false, status = nothing)
    @unpack Q,θ,iterations = lshaped.data
    @unpack keep, offset, indent = lshaped.parameters
    lshaped.Q_history[t] = Q
    lshaped.θ_history[t] = θ
    if status != nothing && lshaped.parameters.log
        lshaped.progress.thresh = Inf
        lshaped.progress.printed = true
        val = if status == MOI.INFEASIBLE
            Inf
        elseif status == MOI.DUAL_INFEASIBLE
            -Inf
        else
            0.0
        end
        ProgressMeter.update!(lshaped.progress, val,
                              showvalues = [
                                  ("$(indentstr(indent))Objective", val),
                                  ("$(indentstr(indent))Early termination", status),
                                  ("$(indentstr(indent))Number of cuts", num_cuts(lshaped)),
                                  ("$(indentstr(indent))Iterations", iterations)
                              ], keep = keep, offset = offset)
        return nothing
    end
    log_regularization!(lshaped,t)
    if lshaped.parameters.log
        current_gap = optimal ? 0.0 : gap(lshaped)
        ProgressMeter.update!(lshaped.progress, current_gap,
                              showvalues = [
                                  ("$(indentstr(indent))Objective", objective_value(lshaped)),
                                  ("$(indentstr(indent))Gap", current_gap),
                                  ("$(indentstr(indent))Number of cuts", num_cuts(lshaped)),
                                  ("$(indentstr(indent))Iterations", iterations)
                              ], keep = keep, offset = offset)
    end
    return nothing
end

function indentstr(n::Integer)
    return repeat(" ", n)
end

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

# Cut functions #
# ======================================================================== #
active(lshaped::AbstractLShaped, hyperplane::AbstractHyperPlane) = active(hyperplane, decision(lshaped), tolerance(lshaped))
active(lshaped::AbstractLShaped, cut::HyperPlane{OptimalityCut}) = optimal(cut, decision(lshaped), model_objectives(lshaped)[cut.id], tolerance(lshaped))
active(lshaped::AbstractLShaped, cut::AggregatedOptimalityCut) = optimal(cut, decision(lshaped), sum(model_objectives(lshaped)[cut.ids]), tolerance(lshaped))
satisfied(lshaped::AbstractLShaped, hyperplane::AbstractHyperPlane) = satisfied(hyperplane, decision(lshaped), tolerance(lshaped))
satisfied(lshaped::AbstractLShaped, cut::HyperPlane{OptimalityCut}) = satisfied(cut, decision(lshaped), model_objectives(lshaped)[cut.id], tolerance(lshaped))
satisfied(lshaped::AbstractLShaped, cut::AggregatedOptimalityCut) = satisfied(cut, decision(lshaped), sum(model_objectives(lshaped)[cut.ids]), tolerance(lshaped))
violated(lshaped::AbstractLShaped, hyperplane::AbstractHyperPlane) = !satisfied(lshaped, hyperplane)
gap(lshaped::AbstractLShaped, hyperplane::AbstractHyperPlane) = gap(hyperplane, decision(lshaped))
gap(lshaped::AbstractLShaped, cut::HyperPlane{OptimalityCut}) = gap(cut, decision(lshaped), model_objectives(lshaped)[cut.id])
gap(lshaped::AbstractLShaped, cut::AggregatedOptimalityCut) = gap(cut, decision(lshaped), sum(model_objectives(lshaped)[cut.ids]))

function add_cut!(lshaped::AbstractLShaped, cut::AbstractHyperPlane; consider_consolidation = true, check = true)
    added = add_cut!(lshaped, cut, model_objectives(lshaped), subobjectives(lshaped), check = check)
    if consider_consolidation
        added && add_cut!(lshaped, lshaped.consolidation, cut)
    end
    return added
end

function add_cut!(lshaped::AbstractLShaped, cut::HyperPlane{OptimalityCut}, θs::AbstractVector, subobjectives::AbstractVector, Q::AbstractFloat; check = true)
    if lshaped.master_variables[cut.id].value == 0
        # Add master variable if cut is encountered for the first time
        add_master_variable!(lshaped, cut.id)
    end
    # Get the model value
    θ = θs[cut.id]
    @unpack cut_scaling = lshaped.parameters
    # Update objective
    subobjectives[cut.id] = Q
    # Check if cut gives new information
    if check && θ > -Inf && (θ + sqrt(eps()) >= Q || θ + sqrt(eps()) >= cut(lshaped.x))
        # Optimal with respect to this subproblem
        return false
    end
    # Add optimality cut
    process_cut!(lshaped, cut)
    f, set = moi_constraint(cut, lshaped.master_variables, cut_scaling)
    push!(lshaped.cut_constraints, MOI.add_constraint(lshaped.master, f, set))
    lshaped.data.num_cuts += 1
    if lshaped.parameters.debug
        push!(lshaped.cuts, cut)
    end
    return true
end
function add_cut!(lshaped::AbstractLShaped, cut::AggregatedOptimalityCut, θs::AbstractVector, subobjectives::AbstractVector, Q::AbstractFloat; check = true)
    for id in cut.ids
        if lshaped.master_variables[id].value == 0
            # Add master variable if cut is encountered for the first time
            add_master_variable!(lshaped, id)
        end
    end
    θs = θs[cut.ids]
    θ = sum(θs)
    @unpack cut_scaling = lshaped.parameters
    # Update objective
    subobjectives[cut.ids] .= Q/length(cut.ids)
    # Check if cut gives new information
    if check && θ > -Inf && (θ + sqrt(eps()) >= Q || θ + sqrt(eps()) >= cut(lshaped.x))
        # Optimal with respect to these subproblems
        return false
    end
    # Add optimality cut
    process_cut!(lshaped, cut)
    f, set = moi_constraint(cut, lshaped.master_variables, cut_scaling)
    push!(lshaped.cut_constraints, MOI.add_constraint(lshaped.master, f, set))
    lshaped.data.num_cuts += 1
    if lshaped.parameters.debug
        push!(lshaped.cuts, cut)
    end
    return true
end
add_cut!(lshaped::AbstractLShaped, cut::AnyOptimalityCut, θs::AbstractVector, subobjectives::AbstractVector, x::AbstractVector; check = true) = add_cut!(lshaped, cut, θs, subobjectives, cut(x); check = check)
add_cut!(lshaped::AbstractLShaped, cut::AnyOptimalityCut, θs::AbstractVector, subobjectives::AbstractVector; check = true) = add_cut!(lshaped, cut, θs, subobjectives, current_decision(lshaped); check = check)

function add_cut!(lshaped::AbstractLShaped, cut::HyperPlane{FeasibilityCut}, ::AbstractVector, subobjectives::AbstractVector, Q::AbstractFloat; check = true)
    # Ensure that there is no false convergence
    subobjectives[cut.id] = Q
    # Add feasibility cut
    process_cut!(lshaped, cut)
    f, set = moi_constraint(cut, lshaped.master_variables, 1.0)
    push!(lshaped.cut_constraints, MOI.add_constraint(lshaped.master, f, set))
    lshaped.data.num_cuts += 1
    if lshaped.parameters.debug
        push!(lshaped.feasibility.cuts, cut)
    end
    return true
end
add_cut!(lshaped::AbstractLShaped, cut::HyperPlane{FeasibilityCut}, θs::AbstractVector, subobjectives::AbstractVector; check = true) = add_cut!(lshaped, cut, θs, subobjectives, Inf)

function add_cut!(lshaped::AbstractLShaped, cut::HyperPlane{Infeasible}, ::AbstractVector, subobjectives::AbstractVector; check = true)
    subobjectives[cut.id] = Inf
    return true
end

function add_cut!(lshaped::AbstractLShaped, cut::HyperPlane{Unbounded}, ::AbstractVector, subobjectives::AbstractVector; check = true)
    subobjectives[cut.id] = -Inf
    return true
end


#####
##### apply flux boundary conditions to tracer tendency `Gc`
#####

# a positive west flux is associated with an *increase* in `Gc` near the west boundary.
# same for south and bottom.
@kernel function apply_west_bcs_kernel!(Gc, grid, west_bc, iter, ΔT)
    j, k = @index(Global, NTuple)
    jj = j + grid.Hy
    kk = k + grid.Hz
    @inbounds Gc[1+grid.Hx, jj, kk] += getbc(west_bc, j, k, iter) * ΔT * Ax(1+grid.Hx, jj, kk, grid) / volume(1+grid.Hx, jj, kk, grid)
end

@kernel function apply_south_bcs_kernel!(Gc, grid, south_bc, iter, ΔT)
    i, k = @index(Global, NTuple)
    ii = i + grid.Hx
    kk = k + grid.Hz
    @inbounds Gc[ii, 1+grid.Hy, kk] += getbc(south_bc, i, k, iter) * ΔT * Ay(ii, 1+grid.Hy, kk, grid) / volume(ii, 1+grid.Hy, kk, grid)
end

@kernel function apply_bottom_bcs_kernel!(Gc, grid, bottom_bc, iter, ΔT)
    i, j = @index(Global, NTuple)
    ii = i + grid.Hx
    jj = j + grid.Hy
    @inbounds Gc[ii, jj, grid.Nz+grid.Hz] += getbc(bottom_bc, i, j, iter) * ΔT * Az(ii, jj, grid.Nz+grid.Hz, grid) / volume(ii, jj, grid.Nz+grid.Hz, grid)
end

# a positive east flux is associated with an *decrease* in `Gc` near the east boundary.
# same for north and top.
@kernel function apply_east_bcs_kernel!(Gc, grid, east_bc, iter, ΔT)
    j, k = @index(Global, NTuple)
    jj = j + grid.Hy
    kk = k + grid.Hz
    @inbounds Gc[grid.Nx+grid.Hx, jj, kk] -= getbc(east_bc, j, k, iter) * ΔT * Ax(grid.Nx+grid.Hx, jj, kk, grid) / volume(grid.Nx+grid.Hx, jj, kk, grid)
end

@kernel function apply_north_bcs_kernel!(Gc, grid, north_bc, iter, ΔT)
    i, k = @index(Global, NTuple)
    ii = i + grid.Hx
    kk = k + grid.Hz
    @inbounds Gc[ii, grid.Ny+grid.Hy, kk] -= getbc(north_bc, i, k, iter) * ΔT * Ay(ii, grid.Ny+grid.Hy, kk, grid) / volume(ii, grid.Ny+grid.Hy, kk, grid)
end

@kernel function apply_top_bcs_kernel!(Gc, grid, top_bc, iter, ΔT)
    i, j = @index(Global, NTuple)
    ii = i + grid.Hx
    jj = j + grid.Hy
    @inbounds Gc[ii, jj, 1+grid.Hz] -= getbc(top_bc, i, j, iter) * ΔT * Az(ii, jj, 1+grid.Hz, grid) / volume(ii, jj, 1+grid.Hz, grid)
end

##### apply flux boundary conditions in x direction (west and east)
function apply_west_bcs!(Gc, grid, west_bc, iter, ΔT, arch, dep)
    kernel! = apply_west_bcs_kernel!(device(arch), (16,16), (grid.Ny, grid.Nz))
    event = kernel!(Gc, grid, west_bc, iter, ΔT, dependencies=dep)
    wait(device(arch), event)
    return nothing
end
function apply_east_bcs!(Gc, grid, east_bc, iter, ΔT, arch, dep)
    kernel! = apply_east_bcs_kernel!(device(arch), (16,16), (grid.Ny, grid.Nz))
    event = kernel!(Gc, grid, east_bc, iter, ΔT, dependencies=dep)
    wait(device(arch), event)
    return nothing
end

##### apply flux boundary conditions in y direction (south and north)
function apply_south_bcs!(Gc, grid, south_bc, iter, ΔT, arch, dep)
    kernel! = apply_south_bcs_kernel!(device(arch), (16,16), (grid.Nx, grid.Nz))
    event = kernel!(Gc, grid, south_bc, iter, ΔT, dependencies=dep)
    wait(device(arch), event)
    return nothing
end
function apply_north_bcs!(Gc, grid, north_bc, iter, ΔT, arch, dep)
    kernel! = apply_north_bcs_kernel!(device(arch), (16,16), (grid.Nx, grid.Nz))
    event = kernel!(Gc, grid, north_bc, iter, ΔT, dependencies=dep)
    wait(device(arch), event)
    return nothing
end

##### apply flux boundary conditions in z direction (bottom and top)
function apply_bottom_bcs!(Gc, grid, bottom_bc, iter, ΔT, arch, dep)
    kernel! = apply_bottom_bcs_kernel!(device(arch), (16,16), (grid.Nx, grid.Ny))
    event = kernel!(Gc, grid, bottom_bc, iter, ΔT, dependencies=dep)
    wait(device(arch), event)
    return nothing
end
function apply_top_bcs!(Gc, grid, top_bc, iter, ΔT, arch, dep)
    kernel! = apply_top_bcs_kernel!(device(arch), (16,16), (grid.Nx, grid.Ny))
    event = kernel!(Gc, grid, top_bc, iter, ΔT, dependencies=dep)
    wait(device(arch), event)
    return nothing
end

##### apply flux boundary conditions in all three directions, six faces.
function apply_bcs!(Gcs, nuts, grid, iter, ΔT, arch)
    barrier = Event(device(arch))
    events = []
    for name in nut_names
        west_event   =   apply_west_bcs!(Gcs[name].data, grid, nuts[name].bc.west,   iter, ΔT, arch, barrier)
        east_event   =   apply_east_bcs!(Gcs[name].data, grid, nuts[name].bc.east,   iter, ΔT, arch, barrier)
        south_event  =  apply_south_bcs!(Gcs[name].data, grid, nuts[name].bc.south,  iter, ΔT, arch, barrier)
        north_event  =  apply_north_bcs!(Gcs[name].data, grid, nuts[name].bc.north,  iter, ΔT, arch, barrier)
        bottom_event = apply_bottom_bcs!(Gcs[name].data, grid, nuts[name].bc.bottom, iter, ΔT, arch, barrier)
        top_event    =    apply_top_bcs!(Gcs[name].data, grid, nuts[name].bc.top,    iter, ΔT, arch, barrier)
        push!(events, west_event, east_event, south_event, north_event, bottom_event, top_event)
    end

    # filter out ::Nothing
    events = filter(e -> typeof(e) <: Event, events)

    wait(device(arch), MultiEvent(Tuple(events)))
    return nothing
end

# Avoid some computation / memory accesses when no flux boundary conditions will be applied
  apply_west_bcs!(Gc, grid, ::Nothing, iter, ΔT, arch, dep) = nothing
  apply_east_bcs!(Gc, grid, ::Nothing, iter, ΔT, arch, dep) = nothing
 apply_north_bcs!(Gc, grid, ::Nothing, iter, ΔT, arch, dep) = nothing
 apply_south_bcs!(Gc, grid, ::Nothing, iter, ΔT, arch, dep) = nothing
apply_bottom_bcs!(Gc, grid, ::Nothing, iter, ΔT, arch, dep) = nothing
   apply_top_bcs!(Gc, grid, ::Nothing, iter, ΔT, arch, dep) = nothing


"""
    struct JSONModel <: MetabolicModel
        json::Dict{String,Any}
    end

A struct used to store the contents of a JSON model, i.e. a model read from a
file ending with `.json`. These model files typically store all the model
parameters in arrays of JSON objects (i.e. Julia dictionaries).

Usually, not all of the fields of the input JSON can be easily represented when
converting to other models, care should be taken to avoid losing information.

Direct work on this precise model type is not very efficient, as the accessor
functions need to repeatedly find the information in the JSON tree. This gets
very slow especially if calling many accessor functions sequentially. To avoid
that, convert to e.g. [`StandardModel`](@ref) as soon as possible.

# Example
````
model = load_json_model("some_model.json")
model.json # see the actual underlying JSON
reactions(model) # see the list of reactions
````
"""
struct JSONModel <: MetabolicModel
    json::Dict{String,Any}
end

# helper access macros, see examples below for usage
macro _json_sectionkey(namesConstant::Symbol, error)
    esc(:(
        begin
            _key = _guesskey(keys(model.json), _constants.keynames.$namesConstant)
            if isnothing(_key)
                $error
            end
            _key
        end
    ))
end

macro _json_section(namesConstant::Symbol, error)
    esc(:(
        begin
            _key = @_json_sectionkey $namesConstant ($error)
            model.json[_key]
        end
    ))
end

macro _json_section_firstid(namesConstant::Symbol, id::Symbol, error)
    return esc(:(
        begin
            _s = @_json_section $namesConstant ($error)
            _s[findfirst(x -> x["id"] == $id, _s)]
        end
    ))
end

function _parse_annotations(x)::Annotations
    Annotations([k => if typeof(v) == String
        [v]
    else
        convert(Vector{String}, v)
    end for (k, v) in x])
end

_parse_notes(x)::Notes = _parse_annotations(x)

"""
    reactions(model::JSONModel)

Extract reaction names (stored as `.id`) from JSON model.
"""
function reactions(model::JSONModel)
    rs = @_json_section rxns throw(
        DomainError(keys(model.json), "JSON model has no reaction keys"),
    )

    return [string(get(rs[i], "id", "rxn$i")) for i in eachindex(rs)]
end

"""
    metabolites(model::JSONModel)

Extract metabolite names (stored as `.id`) from JSON model.
"""
function metabolites(model::JSONModel)
    ms = @_json_section mets throw(
        DomainError(keys(model.json), "JSON model has no metabolite keys"),
    )

    return [string(get(ms[i], "id", "met$i")) for i in eachindex(ms)]
end

"""
    genes(model::JSONModel)

Extract gene names from a JSON model.
"""
function genes(model::JSONModel)
    gs = @_json_section genes return []

    return [string(get(gs[i], "id", "gene$i")) for i in eachindex(gs)]
end

"""
    stoichiometry(model::JSONModel)

Get the stoichiometry. Assuming the information is stored in reaction object
under key `.metabolites`.
"""
function stoichiometry(model::JSONModel)
    rxn_ids = reactions(model)
    met_ids = metabolites(model)

    rs = @_json_section rxns throw(
        DomainError(keys(model.json), "JSON model has no reaction keys"),
    )

    S = SparseArrays.spzeros(length(met_ids), length(rxn_ids))
    for (i, rid) in enumerate(rxn_ids)
        r = rs[findfirst(x -> x["id"] == rid, rs)]
        for (met_id, coeff) in r["metabolites"]
            j = findfirst(==(met_id), met_ids)
            if isnothing(j)
                throw(
                    DomainError(
                        met_id,
                        "Unknown metabolite found in stoichiometry of $(rxn_ids[i])",
                    ),
                )
            end
            S[j, i] = coeff
        end
    end
    return S
end

"""
    bounds(model::JSONModel)

Get the bounds for reactions, assuming the information is stored in
`.lower_bound` and `.upper_bound`.
"""
function bounds(model::JSONModel)
    rs = @_json_section rxns return (
        sparse(fill(-_constants.default_reaction_bound, n_reactions(model))),
        sparse(fill(_constants.default_reaction_bound, n_reactions(model))),
    )
    return (
        sparse([get(rxn, "lower_bound", -_constants.default_reaction_bound) for rxn in rs]),
        sparse([get(rxn, "upper_bound", _constants.default_reaction_bound) for rxn in rs]),
    )
end

"""
    objective(model::JSONModel)

Collect `.objective_coefficient` keys from model reactions.
"""
function objective(model::JSONModel)
    rs = @_json_section rxns return spzeros(n_reactions(model))

    return sparse([float(get(rxn, "objective_coefficient", 0.0)) for rxn in rs])
end

"""
    reaction_gene_associaton(model::JSONModel, rid::String)

Parses the `.gene_reaction_rule` from reactions.
"""
function reaction_gene_association(model::JSONModel, rid::String)
    rxn = @_json_section_firstid rxns rid return nothing
    return _maybemap(_parse_grr, get(rxn, "gene_reaction_rule", nothing))
end

"""
    reaction_subsystem(model::JSONModel, rid::String)

Parses the `.subsystem` out from reactions.
"""
function reaction_subsystem(model::JSONModel, rid::String)
    rxn = @_json_section_firstid rxns rid return nothing
    return get(rxn, "subsystem", nothing)
end

"""
    metabolite_formula(model::JSONModel, mid::String)

Parse and return the metabolite `.formula`
"""
function metabolite_formula(model::JSONModel, mid::String)
    met = @_json_section_firstid mets mid return nothing
    return _maybemap(_parse_formula, get(met, "formula", nothing))
end

"""
    metabolite_charge(model::JSONModel, mid::String)

Return the metabolite `.charge`
"""
function metabolite_charge(model::JSONModel, mid::String)
    met = @_json_section_firstid mets mid return nothing
    return get(met, "charge", 0)
end

"""
    metabolite_compartment(model::JSONModel, mid::String)

Return the metabolite `.compartment`
"""
function metabolite_compartment(model::JSONModel, mid::String)
    met = @_json_section_firstid mets mid return nothing
    return get(met, "compartment", nothing)
end

"""
    gene_annotations(model::JSONModel, gid::String)::Annotations

Gene annotations from the [`JSONModel`](@ref).
"""
function gene_annotations(model::JSONModel, gid::String)::Annotations
    gene = @_json_section_firstid genes gid return Dict()
    _maybemap(_parse_annotations, get(gene, "annotation", nothing))
end

"""
    gene_notes(model::JSONModel, gid::String)::Notes

Gene notes from the [`JSONModel`](@ref).
"""
function gene_notes(model::JSONModel, gid::String)::Notes
    gene = @_json_section_firstid genes gid return Dict()
    _maybemap(_parse_notes, get(gene, "notes", nothing))
end

"""
    reaction_annotations(model::JSONModel, rid::String)::Annotations

Reaction annotations from the [`JSONModel`](@ref).
"""
function reaction_annotations(model::JSONModel, rid::String)::Annotations
    rxn = @_json_section_firstid rxns rid return Dict()
    _maybemap(_parse_annotations, get(rxn, "annotation", nothing))
end

"""
    reaction_notes(model::JSONModel, rid::String)::Notes

Reaction notes from the [`JSONModel`](@ref).
"""
function reaction_notes(model::JSONModel, rid::String)::Notes
    rxn = @_json_section_firstid rxns rid return Dict()
    _maybemap(_parse_notes, get(rxn, "notes", nothing))
end

"""
    metabolite_annotations(model::JSONModel, mid::String)::Annotations

Metabolite annotations from the [`JSONModel`](@ref).
"""
function metabolite_annotations(model::JSONModel, mid::String)::Annotations
    met = @_json_section_firstid mets mid return Dict()
    _maybemap(_parse_annotations, get(met, "annotation", nothing))
end

"""
    metabolite_notes(model::JSONModel, mid::String)::Notes

Metabolite notes from the [`JSONModel`](@ref).
"""
function metabolite_notes(model::JSONModel, mid::String)::Notes
    met = @_json_section_firstid mets mid return Dict()
    _maybemap(_parse_notes, get(met, "notes", nothing))
end

"""
    reaction_stoichiometry(model::JSONModel, rxn_id::String)::Dict{String, Float64}

Return the reaction equation of reaction with id `rxn_id` in model. The reaction
equation maps metabolite ids to their stoichiometric coefficients.
"""
function reaction_stoichiometry(m::JSONModel, rxn_id::String)::Dict{String, Float64}
    ind = findfirst(x -> x["id"] == rxn_id, m.json["reactions"])
    m.json["reactions"][ind]["metabolites"]
end

"""
    Base.convert(::Type{JSONModel}, mm::MetabolicModel)

Convert any [`MetabolicModel`](@ref) to [`JSONModel`](@ref).
"""
function Base.convert(::Type{JSONModel}, mm::MetabolicModel)
    if typeof(mm) == JSONModel
        return mm
    end

    rxn_ids = reactions(mm)
    met_ids = metabolites(mm)
    gene_ids = genes(mm)
    S = stoichiometry(mm)
    lbs, ubs = bounds(mm)
    ocs = objective(mm)

    json_model = JSONModel(Dict{String,Any}())
    json = Dict{String,Any}()
    json["id"] = "model" # default

    #TODO: add notes, names and similar fun stuff when they are available

    json[first(_constants.keynames.genes)] = [
        Dict([
            "id" => gid,
            "annotation" => gene_annotations(mm, gid),
            "notes" => gene_notes(mm, gid),
        ],) for gid in gene_ids
    ]

    json[first(_constants.keynames.mets)] = [
        Dict([
            "id" => mid,
            "formula" => _maybemap(_unparse_formula, metabolite_formula(mm, mid)),
            "charge" => metabolite_charge(mm, mid),
            "compartment" => metabolite_compartment(mm, mid),
            "annotation" => metabolite_annotations(mm, mid),
            "notes" => metabolite_notes(mm, mid),
        ]) for mid in met_ids
    ]

    json[first(_constants.keynames.rxns)] = [
        begin
            res = Dict{String,Any}()
            res["id"] = rid
            res["subsystem"] = reaction_subsystem(mm, rid)
            res["annotation"] = reaction_annotations(mm, rid)
            res["notes"] = reaction_notes(mm, rid)

            grr = reaction_gene_association(mm, rid)
            if !isnothing(grr)
                res["gene_reaction_rule"] = _unparse_grr(String, grr)
            end

            res["lower_bound"] = lbs[ri]
            res["upper_bound"] = ubs[ri]
            res["objective_coefficient"] = ocs[ri]
            I, V = findnz(S[:, ri])
            res["metabolites"] =
                Dict{String,Float64}([met_ids[ii] => vv for (ii, vv) in zip(I, V)])
            res
        end for (ri, rid) in enumerate(rxn_ids)
    ]

    return JSONModel(json)
end


export LazyDiscretePost,
       ApproximatingDiscretePost

import LazySets.use_precise_ρ

"""
    LazyDiscretePost <: DiscretePost

Textbook implementation of a discrete post operator, but with lazy intersections.

### Fields

- `options` -- an `Options` structure that holds the algorithm-specific options

### Algorithm

The algorithm is based on [Flowpipe-Guard Intersection for Reachability
Computations with Support Functions](http://spaceex.imag.fr/sites/default/files/frehser_adhs2012.pdf).
"""
struct LazyDiscretePost <: DiscretePost
    options::Options

    function LazyDiscretePost(𝑂::Options)
        𝑂copy = copy(𝑂)
        # TODO: pass 𝑂 directly?
        check_aliases_and_add_default_value!(𝑂.dict, 𝑂copy.dict, [:check_invariant_intersection], false)
        check_aliases_and_add_default_value!(𝑂.dict, 𝑂copy.dict, [:overapproximation], Hyperrectangle)
        check_aliases_and_add_default_value!(𝑂.dict, 𝑂copy.dict, [:lazy_R⋂I], false)
        check_aliases_and_add_default_value!(𝑂.dict, 𝑂copy.dict, [:lazy_R⋂G], true)
        check_aliases_and_add_default_value!(𝑂.dict, 𝑂copy.dict, [:lazy_A⌜R⋂G⌟], "invertible")
        check_aliases_and_add_default_value!(𝑂.dict, 𝑂copy.dict, [:lazy_A⌜R⋂G⌟⋂I], true)
        check_aliases_and_add_default_value!(𝑂.dict, 𝑂copy.dict, [:combine_invariant_guard], 𝑂copy[:lazy_R⋂I])

        if 𝑂copy[:combine_invariant_guard] && !𝑂copy[:lazy_R⋂I]
            throw(ArgumentError("option :combine_invariant_guard only makes " *
                                "sense in combination with option :lazy_R⋂I"))
        end

        return new(𝑂copy)
    end
end

# convenience constructor from pairs of symbols
LazyDiscretePost(𝑂::Pair{Symbol,<:Any}...) = LazyDiscretePost(Options(Dict{Symbol,Any}(𝑂)))

# default options for the LazyDiscretePost discrete post operator
LazyDiscretePost() = LazyDiscretePost(Options())

"""
    ApproximatingDiscretePost()

Textbook implementation of a discrete post operator, but with lazy intersections
followed by an overapproximation. This is a particular case of the
`LazyDiscretePost`.
"""
function ApproximatingDiscretePost()
    return LazyDiscretePost(:check_invariant_intersection=>false,
                            :overapproximation=>Hyperrectangle,
                            :lazy_R⋂I=>false,
                            :lazy_R⋂G=>false,
                            :lazy_A⌜R⋂G⌟⋂I=>false)
end

function ApproximatingDiscretePost(𝑂::Options)
    𝑂_default = Options(:lazy_R⋂I=>false,
                        :lazy_R⋂G=>false,
                        :lazy_A⌜R⋂G⌟⋂I=>false)
    merge!(𝑂_default, 𝑂)
    LazyDiscretePost(𝑂_default)
end

init(𝒫::LazyDiscretePost, 𝒮::AbstractSystem, 𝑂::Options) = init!(𝒫, 𝒮, copy(𝑂))

# TODO: use 𝑂 only?
function init!(𝒫::LazyDiscretePost, 𝒮::AbstractSystem, 𝑂::Options)
    𝑂[:n] = statedim(𝒮, 1)

    # solver-specific options (adds default values for unspecified options)
    𝑂out = validate_solver_options_and_add_default_values!(𝑂)

    return 𝑂out
end

function tube⋂inv(𝒫::LazyDiscretePost,
                  reach_tube::Vector{<:AbstractReachSet{<:LazySet{N}}},
                  invariant,
                  start_interval
                 ) where {N}

    dirs = 𝒫.options[:overapproximation]

    Rsets = Vector{AbstractReachSet{<:LazySet{N}}}()
    @inbounds for reach_set in reach_tube
        R⋂I = Intersection(set(reach_set), invariant)
        if 𝒫.options[:check_invariant_intersection] && isempty(R⋂I)
            break
        end
        if !𝒫.options[:lazy_R⋂I]
            R⋂I = overapproximate(R⋂I, dirs)
        end
        push!(Rsets,
              substitute(reach_set, set=R⋂I,
                         time_start=time_start(reach_set) + start_interval[1],
                         time_end=time_end(reach_set) + start_interval[2]))
    end

    return Rsets
end

function post(𝒫::LazyDiscretePost,
              HS::HybridSystem,
              waiting_list::Vector{Tuple{Int, <:AbstractReachSet{LazySet{N}}, Int}},
              passed_list,
              source_loc_id,
              tube⋂inv,
              jumps,
              options
             ) where {N}
    jumps += 1
    oa = 𝒫.options[:overapproximation]
    source_invariant = HS.modes[source_loc_id].X
    inv_isa_Hrep, inv_isa_H_polytope = get_Hrep_info(source_invariant)

    for trans in out_transitions(HS, source_loc_id)
        info("Considering transition: $trans")
        target_loc_id = target(HS, trans)
        target_loc = HS.modes[target(HS, trans)]
        target_invariant = target_loc.X
        constrained_map = resetmap(HS, trans)
        guard = stateset(constrained_map)

        guard_isa_Hrep, guard_isa_H_polytope = get_Hrep_info(guard)
        combine_constraints = 𝒫.options[:combine_invariant_guard] &&
                              inv_isa_Hrep && guard_isa_Hrep
        if combine_constraints # combine the constraints of invariant and guard
            T = inv_isa_H_polytope || guard_isa_H_polytope ? HPolytope : HPolyhedron
            # TODO: remove redundant constraints => use intersection(..)
            invariant_guard = T([constraints_list(source_invariant);
                                 constraints_list(guard)])
        end

        # perform jumps
        post_jump = Vector{ReachSet{LazySet{N}}}()
        sizehint!(post_jump, length(tube⋂inv))
        for reach_set in tube⋂inv
            # check intersection with guard
            if combine_constraints
                R⋂G = Intersection(set(reach_set).X, invariant_guard)
            else
                R⋂G = Intersection(set(reach_set), guard)
            end
            if isempty(R⋂G)
                continue
            end
            if !𝒫.options[:lazy_R⋂G]
                R⋂G = overapproximate(R⋂G, oa)
            end

            # apply assignment
            A⌜R⋂G⌟ = apply_assignment(𝒫, constrained_map, R⋂G)
            if 𝒫.options[:lazy_A⌜R⋂G⌟] == "always" ||
                    (𝒫.options[:lazy_A⌜R⋂G⌟] == "invertible" &&
                     !isinvertible(constrained_map))
                A⌜R⋂G⌟ = overapproximate(A⌜R⋂G⌟, oa)
            end

            # intersect with target invariant
            A⌜R⋂G⌟⋂I = Intersection(target_invariant, A⌜R⋂G⌟)
            if isempty(A⌜R⋂G⌟⋂I)
                continue
            end
            if !𝒫.options[:lazy_A⌜R⋂G⌟⋂I]
                A⌜R⋂G⌟⋂I = overapproximate(A⌜R⋂G⌟⋂I, oa)
            end

            # store result
            push!(post_jump, ReachSet{LazySet{N}}(A⌜R⋂G⌟⋂I,
                                                  time_start(reach_set),
                                                  time_end(reach_set)))
        end

        postprocess(𝒫, HS, post_jump, options, waiting_list, passed_list,
            target_loc_id, jumps)
    end
end

function get_Hrep_info(set::LazySet)
    return (false, false)
end

function get_Hrep_info(set::AbstractPolytope)
    return (true, true)
end

function get_Hrep_info(set::AbstractPolyhedron)
    return (true, false)
end

# --- line-search policies ---

# usually do not use line search
function use_precise_ρ(𝒫::LazyDiscretePost,
                       cap::Intersection{N})::Bool where N<:Real
    return false
end

# use line search for the outermost level, which is a LinearMap
function use_precise_ρ(𝒫::LazyDiscretePost,
                       cap::Intersection{N, <:LinearMap{N}}
                       )::Bool where N<:Real
    return true
end