include("table_utils.jl")

"""
    simulate_waveforms( mcevents::TypedTables.Table, sim::Simulation{T}; kwargs...)
    simulate_waveforms( mcevents::TypedTables.Table, sim::Simulation{T}, output_dir::AbstractString, output_base_name::AbstractString; kwargs...)

Simulates the waveforms for all events defined in `mcevents` for a given [`Simulation`](@ref) by

1. calculating the drift paths of all energy hits defined in `mcevents`,
2. determining the signal (waveforms) for each [`Contact`](@ref),
    for which a [`WeightingPotential`](@ref) is specified in `sim.weighting_potentials`.

## Arguments
* `mcevents::TypedTables.Table`: Table with information about events in the simulated setup.
* `sim::Simulation{T}`: [`Simulation`](@ref) which defines the setup in which the charges in `mcevents` should drift.

If [HDF5.jl](https://github.com/JuliaIO/HDF5.jl) is loaded, this function has additional arguments.

## Additional Arguments (HDF5)
* `output_dir::AbstractString`: Directory where the HDF5 output file is saved.
* `output_base_name::AbstractString`: Basename of the HDF5 output file, default is `"generated_waveforms"`.

## Keywords
* `max_nsteps::Int = 1000`: Maximum number of steps in the drift of each hit.
* `Δt::RealQuantity = 4u"ns"`: Time step used for the drift.
* `diffusion::Bool = false`: Activate or deactive diffusion of charge carriers via random walk.
* `self_repulsion::Bool = false`: Activate or deactive self-repulsion of charge carriers of the same type.
* `number_of_carriers::Int = 1`: Number of charge carriers to be used in the N-Body simulation of an energy deposition.
* `number_of_shells::Int = 1`: Number of shells around the `center` point of the energy deposition.
* `verbose = false`: Activate or deactivate additional info output.
* `chunk_n_physics_events::Int = 1000` (HDF5 only): Number of events that should be saved in a single HDF5 output file.

## Examples
```julia
simulate_waveforms(mcevents, sim, Δt = 1u"ns", verbose = false)
# => returns the input table `mcevents` with an additional column `waveform` in which the generated waveforms are stored
```
```julia
import HDF5
simulate_waveforms(mcevents, sim, "output_dir", "my_basename", Δt = 1u"ns", verbose = false)
# => simulates the charge drift and saves the output to "output_dir/my_basename_evts_xx.h5"
```
!!! note
    The drift paths are just calculated temporarily and not returned.

!!! note
    Using values with units for `Δt` requires the package [Unitful.jl](https://github.com/PainterQubits/Unitful.jl).
"""
function simulate_waveforms( mcevents::TypedTables.Table, sim::Simulation{T};
                             Δt::RealQuantity = 4u"ns",
                             max_nsteps::Int = 1000,
                             diffusion::Bool = false,
                             self_repulsion::Bool = false,
                             number_of_carriers::Int = 1,
                             number_of_shells::Int = 1,
                             verbose::Bool = false ) where {T <: SSDFloat}
    n_total_physics_events = length(mcevents)
    Δtime = T(to_internal_units(Δt))
    n_contacts = length(sim.detector.contacts)
    S = get_coordinate_system(sim)
    contacts = sim.detector.contacts;
    contact_ids = Int[]
    for contact in contacts if !ismissing(sim.weighting_potentials[contact.id]) push!(contact_ids, contact.id) end end
    wpots_interpolated = [ interpolated_scalarfield(sim.weighting_potentials[id]) for id in contact_ids ];
    electric_field = interpolated_vectorfield(sim.electric_field)

    unitless_energy_to_charge = _convert_internal_energy_to_external_charge(sim.detector.semiconductor.material)
    @info "Detector has $(n_contacts) contact"*(n_contacts != 1 ? "s" : "")
    @info "Table has $(length(mcevents)) physics events ($(sum(map(edeps -> length(edeps), mcevents.edep))) single charge depositions)."

    # First simulate drift paths
    drift_paths_and_edeps = _simulate_charge_drifts(mcevents, sim, Δt, max_nsteps, electric_field,
        diffusion, self_repulsion, number_of_carriers, number_of_shells, verbose)
    drift_paths = map(x -> vcat([vcat(ed...) for ed in x[1]]...), drift_paths_and_edeps)
    edeps = map(x -> vcat([vcat(ed...) for ed in x[2]]...), drift_paths_and_edeps)
    # now iterate over contacts and generate the waveform for each contact
    @info "Generating waveforms..."
    waveforms = map(
        wpot ->  map(
            x -> _generate_waveform(x.dps, to_internal_units.(x.edeps), Δt, Δtime, wpot, S, unitless_energy_to_charge),
            TypedTables.Table(dps = drift_paths, edeps = edeps)
        ),
        wpots_interpolated
    )
    mcevents_chns = map(
        i -> add_column(mcevents, :chnid, fill(contact_ids[i], n_total_physics_events)),
        eachindex(contact_ids)
    )
    mcevents_chns = map(
        i -> add_column(mcevents_chns[i], :waveform, ArrayOfRDWaveforms(waveforms[i])),
        eachindex(waveforms)
    )
    return vcat(mcevents_chns...)
end

_convertEnergyDepsToChargeDeps(pos::AbstractVector{<:SVector{3}}, edep::AbstractVector{<:Quantity}, det::SolidStateDetector; kwargs...) =
    _convertEnergyDepsToChargeDeps([[p] for p in pos], [[e] for e in edep], det; kwargs...)

function _convertEnergyDepsToChargeDeps(pos::AbstractVector{<:AbstractVector}, edep::AbstractVector{<:AbstractVector}, det::SolidStateDetector;
        number_of_carriers::Int = 1, number_of_shells::Int = 1)
    charge_clouds = broadcast(
        iEdep_indep -> broadcast(
            i_together -> begin
                nbcc = NBodyChargeCloud(
                    CartesianPoint(to_internal_units.((pos[iEdep_indep][i_together]))),
                    edep[iEdep_indep][i_together],
                    number_of_carriers,
                    number_of_shells = number_of_shells
                )
                move_charges_inside_semiconductor!([nbcc.locations], [nbcc.energies], det)
                nbcc
            end,
            eachindex(edep[iEdep_indep])
        ),
        eachindex(edep)
    )
    locations = [map(cc -> cc.locations, ccs) for ccs in charge_clouds]
    edeps = [map(cc -> cc.energies, ccs) for ccs in charge_clouds]
    locations, edeps
end

function _simulate_charge_drifts( mcevents::TypedTables.Table, sim::Simulation{T},
                                  Δt::RealQuantity, max_nsteps::Int,
                                  electric_field::Interpolations.Extrapolation,
                                  diffusion::Bool,
                                  self_repulsion::Bool,
                                  number_of_carriers::Int,
                                  number_of_shells::Int,
                                  verbose::Bool ) where {T <: SSDFloat}
    @showprogress map(mcevents) do phyevt
        locations, edeps = _convertEnergyDepsToChargeDeps(phyevt.pos, phyevt.edep, sim.detector; number_of_carriers, number_of_shells)
        drift_paths = map( i -> _drift_charges(sim.detector, sim.electric_field.grid, sim.point_types,
                VectorOfArrays(locations[i]), VectorOfArrays(edeps[i]),
                electric_field, T(Δt.val) * unit(Δt), max_nsteps = max_nsteps,
                diffusion = diffusion, self_repulsion = self_repulsion, verbose = verbose
            ),
            eachindex(edeps)
        )
        drift_paths, edeps
    end
end

function _generate_waveform( drift_paths::Vector{<:EHDriftPath{T}}, charges::Vector{<:SSDFloat}, Δt::RealQuantity, dt::T,
                             wpot::Interpolations.Extrapolation{T, 3}, S::CoordinateSystemType, unitless_energy_to_charge) where {T <: SSDFloat}
    timestamps = _common_timestamps( drift_paths, dt )
    timestamps_with_units = range(zero(Δt), step = Δt, length = length(timestamps))
    signal = zeros(T, length(timestamps))
    add_signal!(signal, timestamps, drift_paths, T.(charges), wpot, S)
    RDWaveform( timestamps_with_units, signal * unitless_energy_to_charge)
end


@inline score( one::A, two::A  ) where {A <: UngappedAlignment} = @fastmath score( one ) + score( two )
@inline identity( one::A, two::A, onelen::Int, twolen::Int ) where {A <: UngappedAlignment} = @fastmath score( one, two ) / ( onelen + twolen )

@inline function sorted_offsets( sa::UnitRange, index::FMIndexes.FMIndex )
   res = Vector{Int}(length(sa))
   ridx = 1
   for sidx in sa
      res[ridx] = FMIndexes.sa_value( sidx, index ) + 1
      ridx += 1
   end
   sort!(res)
   res
end

# Paired version.
function splice_by_score!( one::Vector{A}, two::Vector{A}, threshold, buffer ) where A <: UngappedAlignment
   i = 1
   while i <= length( one )
      if threshold - score( one[i], two[i] ) > buffer
         splice!( one, i )
         splice!( two, i )
         i -= 1
      end
      i += 1
   end
end

function splice_by_identity!( one::Vector{A}, two::Vector{A}, threshold, buffer, onelen, twolen ) where A <: UngappedAlignment
   i = 1
   while i <= length( one )
      if threshold - identity( one[i], two[i], onelen, twolen ) > buffer
         splice!( one, i )
         splice!( two, i )
         i -= 1
      end
      i += 1
   end
end

# Paired ungapped_align
function ungapped_align( p::AlignParam, lib::GraphLib, fwd::FASTQRecord, rev::FASTQRecord; ispos=true, anchor_left=true )

   fwd_seed,fwd_readloc,strand = seed_locate( p, lib.index, fwd, offset_left=anchor_left, both_strands=!p.is_stranded )

   if (p.is_pair_rc && strand) || (!p.is_pair_rc && !strand)
      reverse_complement!(rev)
      rev_seed,rev_readloc = seed_locate( p, lib.index, rev, offset_left=!anchor_left, both_strands=false)
   else
      rev_seed,rev_readloc = seed_locate( p, lib.index, rev, offset_left=anchor_left, both_strands=false)
   end
   fwd_len = length(fwd.sequence)
   rev_len = length(rev.sequence)

   if !strand
      reverse_complement!(fwd)
      ispos = false
   end

   fwd_res      = Nullable{Vector{SGAlignment}}()
   rev_res      = Nullable{Vector{SGAlignment}}()
   maxscore = 0.0

   fwd_sorted = sorted_offsets( fwd_seed, lib.index )
   rev_sorted = sorted_offsets( rev_seed, lib.index )

   fidx,ridx = 1,1

   while fidx <= length(fwd_sorted) && ridx <= length(rev_sorted)

      dist = abs( fwd_sorted[fidx] - rev_sorted[ridx] )
      if dist < p.seed_pair_range # inside allowable pair distance

         geneind = convert( CoordInt, searchsortedlast( lib.offset, fwd_sorted[fidx] ) )
         next_offset= geneind < length(lib.offset) ? lib.offset[geneind+1] :
                                                           lib.offset[geneind] + length(lib.graphs[geneind].seq)
         if lib.offset[geneind] <= rev_sorted[ridx] < next_offset

            fwd_aln = _ungapped_align( p, lib, fwd, fwd_sorted[fidx], fwd_readloc; ispos=ispos, geneind=geneind )
            rev_aln = _ungapped_align( p, lib, rev, rev_sorted[ridx], rev_readloc; ispos=!ispos, geneind=geneind )

            @fastmath const scvar = identity( fwd_aln, rev_aln, fwd_len, rev_len )

            if isvalid(fwd_aln) && isvalid(rev_aln)
               if isnull( fwd_res ) || isnull( rev_res )
                  fwd_res = Nullable(SGAlignment[ fwd_aln ])
                  rev_res = Nullable(SGAlignment[ rev_aln ])
                  maxscore = scvar
               else
               # new best score
                  if scvar > maxscore
                     # better than threshold for all of the previous scores
                     if scvar - maxscore > p.score_range
                        if length( fwd_res.value ) >= 1
                           empty!( fwd_res.value )
                           empty!( rev_res.value )
                        end
                     else # keep at least one previous score
                        splice_by_identity!( fwd_res.value, rev_res.value, scvar, p.score_range, fwd_len, rev_len )
                     end
                     maxscore = scvar
                     push!( fwd_res.value, fwd_aln )
                     push!( rev_res.value, rev_aln )
                  else # new score is lower than previously seen
                     if maxscore - scvar <= p.score_range # but tolerable
                        push!( fwd_res.value, fwd_aln )
                        push!( rev_res.value, rev_aln )
                     end
                  end # end score vs maxscore
               end # end isnull
            end # end isvalid
            ridx += 1
            fidx += 1
            continue
         end # is reverse in same gene as fwd
      end # end dist vs. seed_pair_range
      if fwd_sorted[fidx] > rev_sorted[ridx]
         ridx += 1
      else
         fidx += 1
      end
   end #end while

   fwd_res,rev_res
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.
# *********************************************************************************
using Xpress

@testset "Thermal loads" begin
    m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
    results = run_reopt(m, "./scenarios/thermal_load.json")

    @test round(results["ExistingBoiler"]["year_one_boiler_fuel_consumption_mmbtu"], digits=0) ≈ 2905

    data = JSON.parsefile("./scenarios/thermal_load.json")
    data["DomesticHotWaterLoad"]["fuel_loads_mmbtu_per_hour"] = repeat([0.5], 8760)
    data["SpaceHeatingLoad"]["fuel_loads_mmbtu_per_hour"] = repeat([0.5], 8760)
    s = Scenario(data)
    inputs = REoptInputs(s)
    m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
    results = run_reopt(m, inputs)

    @test round(results["ExistingBoiler"]["year_one_boiler_fuel_consumption_mmbtu"], digits=0) ≈ 8760
end
#=
add a time-of-export rate that is greater than retail rate for the month of January,
check to make sure that PV does NOT export unless the site load is met first for the month of January.
=#
@testset "Do not allow_simultaneous_export_import" begin
    model = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
    data = JSON.parsefile("./scenarios/monthly_rate.json")

    # create wholesale_rate with compensation in January > retail rate
    jan_rate = data["ElectricTariff"]["monthly_energy_rates"][1]
    data["ElectricTariff"]["wholesale_rate"] =
        append!(repeat([jan_rate + 0.1], 31 * 24), repeat([0.0], 8760 - 31*24))
    data["ElectricTariff"]["monthly_demand_rates"] = repeat([0], 12)
    data["ElectricUtility"] = Dict("allow_simultaneous_export_import" => false)

    s = Scenario(data)
    inputs = REoptInputs(s)
    results = run_reopt(model, inputs)

    @test all(x == 0.0 for (i,x) in enumerate(results["ElectricUtility"]["year_one_to_load_series_kw"][1:744])
              if results["PV"]["year_one_to_grid_series_kw"][i] > 0)
end

@testset "Solar and Storage w/BAU" begin
    m1 = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
    m2 = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
    results = run_reopt([m1,m2], "./scenarios/pv_storage.json")

    @test results["PV"]["size_kw"] ≈ 216.6667 atol=0.01
    @test results["PV"]["lcoe_per_kwh"] ≈ 0.0483 atol = 0.001
    @test results["Financial"]["lcc"] ≈ 1.240037e7 rtol=1e-5
    @test results["Financial"]["lcc_bau"] ≈ 12766397 rtol=1e-5
    @test results["Storage"]["size_kw"] ≈ 55.9 atol=0.1
    @test results["Storage"]["size_kwh"] ≈ 78.9 atol=0.1
    proforma_npv = REoptLite.npv(results["Financial"]["offtaker_annual_free_cashflows"] -
        results["Financial"]["offtaker_annual_free_cashflows_bau"], 0.081)
    @test results["Financial"]["npv"] ≈ proforma_npv rtol=0.0001
end

@testset "Outage with Generator, outate simulator, BAU critical load outputs" begin
    m1 = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
    m2 = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
    p = REoptInputs("./scenarios/generator.json")
    results = run_reopt([m1,m2], p)
    @test results["Generator"]["size_kw"] ≈ 8.13 atol=0.01
    @test (sum(results["Generator"]["year_one_to_load_series_kw"][i] for i in 1:9) +
           sum(results["Generator"]["year_one_to_load_series_kw"][i] for i in 13:8760)) == 0
    @test results["ElectricLoad"]["bau_critical_load_met"] == false
    @test results["ElectricLoad"]["bau_critical_load_met_time_steps"] == 0

    simresults = simulate_outages(results, p)
    @test simresults["resilience_hours_max"] == 11
end

@testset "Minimize Unserved Load" begin
    m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
    results = run_reopt(m, "./scenarios/outage.json")

    @test results["expected_outage_cost"] ≈ 0
    @test sum(results["unserved_load_per_outage"]) ≈ 0
    @test value(m[:binMGTechUsed]["Generator"]) == 1
    @test value(m[:binMGTechUsed]["PV"]) == 0
    @test value(m[:binMGStorageUsed]) == 1
    @test results["Financial"]["lcc"] ≈ 7.3879557e7 atol=5e4

    #=
    Scenario with $0/kWh value_of_lost_load_per_kwh, 12x169 hour outages, 1kW load/hour, and min_resil_timesteps = 168
    - should meet 168 kWh in each outage such that the total unserved load is 12 kWh
    =#
    m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
    results = run_reopt(m, "./scenarios/nogridcost_minresilhours.json")
    @test sum(results["unserved_load_per_outage"]) ≈ 12

    # testing dvUnserved load, which would output 100 kWh for this scenario before output fix
    m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
    results = run_reopt(m, "./scenarios/nogridcost_multiscenario.json")
    @test sum(results["unserved_load_per_outage"]) ≈ 60

end

@testset "Multiple Sites" begin
    m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
    ps = [
        REoptInputs("./scenarios/pv_storage.json"),
        REoptInputs("./scenarios/monthly_rate.json"),
    ];
    results = run_reopt(m, ps)
    @test results[3]["Financial"]["lcc"] + results[10]["Financial"]["lcc"] ≈ 1.240037e7 + 437169.0 rtol=1e-5
end

@testset "MPC" begin
    model = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
    r = run_mpc(model, "./scenarios/mpc.json")
    @test maximum(r["ElectricUtility"]["to_load_series_kw"][1:15]) <= 98.0
    @test maximum(r["ElectricUtility"]["to_load_series_kw"][16:24]) <= 97.0
    @test sum(r["PV"]["to_grid_series_kw"]) ≈ 0
end

@testset "Complex Incentives" begin
    """
    This test was compared against the API test:
        reo.tests.test_reopt_url.EntryResourceTest.test_complex_incentives
    when using the hardcoded levelization_factor in this package's REoptInputs function.
    The two LCC's matched within 0.00005%. (The Julia pkg LCC is  1.0971991e7)
    """
    m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
    results = run_reopt(m, "./scenarios/incentives.json")
    @test results["Financial"]["lcc"] ≈ 1.0968526e7 atol=5e4
end

@testset verbose = true "Rate Structures" begin

    @testset "Tiered Energy" begin
        m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
        results = run_reopt(m, "./scenarios/tiered_rate.json")
        @test results["ElectricTariff"]["year_one_energy_cost"] ≈ 2342.88
    end

    @testset "Lookback Demand Charges" begin
        m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
        results = run_reopt(m, "./scenarios/lookback_rate.json")
        @test results["ElectricTariff"]["year_one_demand_cost"] ≈ 721.99
    end

    @testset "Blended tariff" begin
        model = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
        results = run_reopt(model, "./scenarios/no_techs.json")
        @test results["ElectricTariff"]["year_one_energy_cost"] ≈ 1000.0
        @test results["ElectricTariff"]["year_one_demand_cost"] ≈ 136.99
    end

    @testset "Coincident Peak Charges" begin
        model = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
        results = run_reopt(model, "./scenarios/coincident_peak.json")
        @test results["ElectricTariff"]["year_one_coincident_peak_cost"] ≈ 15.0
        @test results["ElectricTariff"]["lifecycle_coincident_peak_cost"] ≈ 15.0 * 12.94887 atol=0.1
    end

    # # tiered monthly demand rate  TODO: expected results?
    # m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
    # data = JSON.parsefile("./scenarios/tiered_rate.json")
    # data["ElectricTariff"]["urdb_label"] = "59bc22705457a3372642da67"
    # s = Scenario(data)
    # inputs = REoptInputs(s)
    # results = run_reopt(m, inputs)

    # TODO test for tiered TOU demand rates
end

@testset "Wind" begin
    m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
    results = run_reopt(m, "./scenarios/wind.json")
    @test results["Wind"]["size_kw"] ≈ 3752 atol=0.1
    @test results["Financial"]["lcc"] ≈ 8.591017e6 rtol=1e-5
    #=
    0.5% higher LCC in this package as compared to API ? 8,591,017 vs 8,551,172
    - both have zero curtailment
    - same energy to grid: 5,839,317 vs 5,839,322
    - same energy to load: 4,160,683 vs 4,160,677
    - same city: Boulder
    - same total wind prod factor

    REoptLite.jl has:
    - bigger turbine: 3752 vs 3735
    - net_capital_costs_plus_om: 8,576,590 vs. 8,537,480

    TODO: will these discrepancies be addressed once NMIL binaries are added?
    =#
end

@testset "Multiple PVs" begin
    m1 = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
    m2 = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
    results = run_reopt([m1,m2], "./scenarios/multiple_pvs.json")

    ground_pv = results["PV"][findfirst(pv -> pv["name"] == "ground", results["PV"])]
    roof_west = results["PV"][findfirst(pv -> pv["name"] == "roof_west", results["PV"])]
    roof_east = results["PV"][findfirst(pv -> pv["name"] == "roof_east", results["PV"])]

    @test ground_pv["size_kw"] ≈ 15 atol=0.1
    @test roof_west["size_kw"] ≈ 7 atol=0.1
    @test roof_east["size_kw"] ≈ 4 atol=0.1
    @test ground_pv["lifecycle_om_cost_bau"] ≈ 782.0 atol=0.1
    @test roof_west["lifecycle_om_cost_bau"] ≈ 782.0 atol=0.1
    @test ground_pv["average_annual_energy_produced_kwh_bau"] ≈ 8844.19 atol=0.1
    @test roof_west["average_annual_energy_produced_kwh_bau"] ≈ 7440.1 atol=0.1
    @test ground_pv["average_annual_energy_produced_kwh"] ≈ 26533.54 atol=0.1
    @test roof_west["average_annual_energy_produced_kwh"] ≈ 10416.52 atol=0.1
    @test roof_east["average_annual_energy_produced_kwh"] ≈ 6482.37 atol=0.1
end

## equivalent REopt Lite API Post for test 2:
#   NOTE have to hack in API levelization_factor to get LCC within 5e-5 (Mosel tol)
# {"Scenario": {
#     "Site": {
#         "longitude": -118.1164613,
#         "latitude": 34.5794343,
#         "roof_squarefeet": 5000.0,
#         "land_acres": 1.0,
#     "PV": {
#         "macrs_bonus_pct": 0.4,
#         "installed_cost_per_kw": 2000.0,
#         "tilt": 34.579,
#         "degradation_pct": 0.005,
#         "macrs_option_years": 5,
#         "federal_itc_pct": 0.3,
#         "module_type": 0,
#         "array_type": 1,
#         "om_cost_per_kw": 16.0,
#         "macrs_itc_reduction": 0.5,
#         "azimuth": 180.0,
#         "federal_rebate_per_kw": 350.0,
#         "dc_ac_ratio": 1.1
#     },
#     "LoadProfile": {
#         "doe_reference_name": "RetailStore",
#         "annual_kwh": 10000000.0,
#         "city": "LosAngeles"
#     },
#     "Storage": {
#         "total_rebate_per_kw": 100.0,
#         "macrs_option_years": 5,
#         "can_grid_charge": true,
#         "macrs_bonus_pct": 0.4,
#         "macrs_itc_reduction": 0.5,
#         "total_itc_pct": 0,
#         "installed_cost_per_kw": 1000.0,
#         "installed_cost_per_kwh": 500.0,
#         "replace_cost_per_kw": 460.0,
#         "replace_cost_per_kwh": 230.0
#     },
#     "ElectricTariff": {
#         "urdb_label": "5ed6c1a15457a3367add15ae"
#     },
#     "Financial": {
#         "escalation_pct": 0.026,
#         "offtaker_discount_pct": 0.081,
#         "owner_discount_pct": 0.081,
#         "analysis_years": 20,
#         "offtaker_tax_pct": 0.4,
#         "owner_tax_pct": 0.4,
#         "om_cost_escalation_pct": 0.025
#     }
# }}}


"""
    RecurrentQNetworkArchitecture
    specify an architecture with the following form:
    [CONV]-[FC]-[LSTM]-[FC]
"""
@with_kw mutable struct RecurrentQNetworkArchitecture
   fc_in::Vector{Int64} = Vector{Int64}[]
   convs::Vector{Tuple{Int64, Vector{Int64}, Int64}} = Vector{Tuple{Int64, Tuple{Int64, Int64}, Int64}}[]
   fc_out::Vector{Int64} = Vector{Int64}[]
   lstm_size::Int64 = 32
end

"""
    DeepRecurrentQLearningSolver
Deep Q learning with a recurrent module to solve POMDPs

## Fields:
- `arch::RecurrentQNetworkArchitecture` Specify the architecture of the Q network default = QNetworkArchitecture(conv=[], fc=[])
- `lr::Float64` learning rate default = 0.005
- `max_steps::Int64` total number of training step default = 1000
- `target_update_freq::Int64` frequency at which the target network is updated default = 500
- `batch_size::Int64` batch size sampled from the replay buffer default = 32
- `trace_length::Int64` trajectory length used to train the LSTM
- `train_freq::Int64` frequency at which the active network is updated default  = 4
- `log_freq::Int64` frequency at which to logg info default = 100
- `eval_freq::Int64` frequency at which to eval the network default = 100
- `num_ep_eval::Int64` number of episodes to evaluate the policy default = 100
- `eps_fraction::Float64` fraction of the training set used to explore default = 0.5
- `eps_end::Float64` value of epsilon at the end of the exploration phase default = 0.01
- `double_q::Bool` double q learning udpate default = true
- `dueling::Bool` dueling structure for the q network default = true
- `prioritized_replay::Bool` enable prioritized experience replay default = true
- `prioritized_replay_alpha::Float64` default = 0.6
- `prioritized_replay_epsilon::Float64` default = 1e-6
- `prioritized_replay_beta::Float64` default = 0.4
- `buffer_size::Int64` size of the experience replay buffer default = 1000
- `max_episode_length::Int64` maximum length of a training episode default = 100
- `train_start::Int64` number of steps used to fill in the replay buffer initially default = 200
- `grad_clip::Bool` enables gradient clipping default = true
- `clip_val::Float64` maximum value for the grad norm default = 10.0
- `rng::AbstractRNG` random number generator default = MersenneTwister(0)
- `verbose::Bool` default = true
"""
@with_kw mutable struct DeepRecurrentQLearningSolver
    arch::RecurrentQNetworkArchitecture = RecurrentQNetworkArchitecture()
    lr::Float64 = 0.001
    max_steps::Int64 = 1000
    target_update_freq::Int64 = 500
    batch_size::Int64 = 32
    trace_length = 6
    train_freq::Int64  = 4
    log_freq::Int64 = 100
    eval_freq::Int64 = 100
    num_ep_eval::Int64 = 100
    eps_fraction::Float64 = 0.5
    eps_end::Float64 = 0.01
    double_q::Bool = true
    dueling::Bool = true
    buffer_size::Int64 = 10000
    max_episode_length::Int64 = 100
    train_start::Int64 = 200
    grad_clip::Bool = true
    clip_val::Float64 = 10.0
    rng::AbstractRNG = MersenneTwister(0)
    logdir::String = "log"
    save_freq::Int64 = 10000
    verbose::Bool = true
end


using .PlotAxes
export logrange

function PlotAxes.asplotable(x::AuditorySpectrogram,args...;quantize=(100,128),
    kwds...)
  args = replace(collect(args),:freq => (:freq => logrange))

  asplotable(getcontents(x),args...;quantize=quantize,kwds...)
end

default_quantize(x) = (100,)
default_quantize(x,y) = (100,128,)
default_quantize(x,y,args...) = (100,128,fill(10,length(args))...)

function PlotAxes.asplotable(x::MetaAxisArray;kwds...)
  asplotable(x,axisnames(x)...;kwds...)
end
function PlotAxes.asplotable(x::MetaAxisArray,ax1,axes...;
  quantize=default_quantize(ax1,axes...),kwds...)
  # TODO: handle time or freq not being present
  args = (ax1,axes...)
  args = replace(collect(args),:freq => (:freq => logrange))
  infront = filter(x -> x !== nothing,indexin([:time,:freq => logrange],args))
  others = setdiff(1:length(args),infront)
  args = args[vcat(infront,others)]

  asplotable(getcontents(x),args...;quantize=quantize,kwds...)
end


module Microbiome

# Samples and Features
export MicrobiomeSample,
       Taxon,
       GeneFunction,
       Metabolite,
       metadata,
       set!,
       unset!,
       insert!,
       delete!,
       name,
       taxrank,
       hasrank,
       taxon,
       hastaxon,
       genefunction,
       commonname,
       masscharge,
       retentiontime

# EcoBase Translations
export abundances,
       nfeatures,
       featurenames,
       getfeature,
       nsamples,
       samplenames,
       getsample,
       AbstractFeature,
       AbstractSample
    #    featureabundances,
    #    sampleabundances

# Profiles
export CommunityProfile,
       featuretype,
       features,
       samples,
       ranks,
       rankfilter,
       profiletype,
       featuretotals,
       sampletotals,
       commjoin,
       metadata

# Abundances
export present,
       prevalence,
       relativeabundance!,
       relativeabundance,
       prevalence_filter
#     filterabund

# Diversity
export ginisimpson,
       shannon,
       ginisimpson!,
       shannon!,
       present,
       prevalence,
       braycurtis,
       jaccard,
       hellinger,
       pcoa

using Statistics
using SparseArrays
using EcoBase
using Dictionaries
using Tables
using Distances
using MultivariateStats
using ReTest

import Dictionaries: set!, unset!, insert!, delete!
import Base: ==

include("ecobase.jl")
include("samples.jl")
include("features.jl")
include("profiles.jl")
include("diversity.jl")
include("comm_joins.jl")

end  # module Microbiome


# Objects here are defined as scratchspace for recurring computations when
# fitting models. The naming indicates the dimensions, e.g. _N, _N2 etc are
# vectors of size N (several as sometimes we need non-overlapping vectors)
# _NC are N*C matrices used in multiclass settings
#
# For now all of these are Float64 scratch spaces.

const SCRATCH_N   = Ref(zeros(0))
const SCRATCH_N2  = Ref(zeros(0))
const SCRATCH_N3  = Ref(zeros(0))
const SCRATCH_P   = Ref(zeros(0))
const SCRATCH_NC  = Ref(zeros(0,0))
const SCRATCH_NC2 = Ref(zeros(0,0))
const SCRATCH_NC3 = Ref(zeros(0,0))
const SCRATCH_NC4 = Ref(zeros(0,0))
const SCRATCH_PC  = Ref(zeros(0,0))

allocate(n, p, c=0) = begin
    SCRATCH_N[]  = zeros(n)
    SCRATCH_N2[] = zeros(n)
    SCRATCH_N3[] = zeros(n)
    SCRATCH_P[]  = zeros(p)
    if !iszero(c)
        SCRATCH_NC[]  = zeros(n, c)
        SCRATCH_NC2[] = zeros(n, c)
        SCRATCH_NC3[] = zeros(n, c)
        SCRATCH_NC4[] = zeros(n, c)
        SCRATCH_PC[]  = zeros(p, c)
    end
end

deallocate() = begin
    SCRATCH_N[]   = zeros(0)
    SCRATCH_N2[]  = zeros(0)
    SCRATCH_N3[]  = zeros(0)
    SCRATCH_P[]   = zeros(0)
    SCRATCH_NC[]  = zeros(0,0)
    SCRATCH_NC2[] = zeros(0,0)
    SCRATCH_NC3[] = zeros(0,0)
    SCRATCH_NC4[] = zeros(0,0)
    SCRATCH_PC[]  = zeros(0,0)
end


module SokobanUndirectedModule

import ..GridWorlds as GW
import Random
import ReinforcementLearningBase as RLBase

#####
##### game logic
#####

const NUM_OBJECTS = 4
const AGENT = 1
const WALL = 2
const BOX = 3
const TARGET = 4
const NUM_ACTIONS = 4

struct LevelDataset
    characters::Array{Char, 3}
end

function LevelDataset(file::AbstractString)
    lines = readlines(file)

    height = 0
    width = 0
    num_levels = 0

    for i in 1 : length(lines)
        if lines[i] == ""
            height = i - 2
            width = length(lines[i - 1])
            num_levels = length(lines) ÷ (height + 2)
            break
        end
    end

    characters = Array{Char}(undef, height, width, num_levels)

    for k in 1 : num_levels
        for j in 1 : width
            for i in 1 : height
                characters[i, j, k] = lines[(k - 1) * (height + 2) + 1 + i][j]
            end
        end
    end

    return LevelDataset(characters)
end

"""
The dataset (000.txt) used is [boxoban-levels](https://github.com/deepmind/boxoban-levels/blob/master/medium/train/000.txt) (LICENSE file included along with 000.txt)

The following is the citation of the dataset:

@misc{boxobanlevels,
author = {Arthur Guez, Mehdi Mirza, Karol Gregor, Rishabh Kabra, Sebastien Racaniere, Theophane Weber, David Raposo, Adam Santoro, Laurent Orseau, Tom Eccles, Greg Wayne, David Silver, Timothy Lillicrap, Victor Valdes},
title = {An investigation of Model-free planning: boxoban levels},
howpublished= {https://github.com/deepmind/boxoban-levels/},
year = "2018",
}
"""
mutable struct SokobanUndirected{R, RNG} <: GW.AbstractGridWorld
    tile_map::BitArray{3}
    agent_position::CartesianIndex{2}
    reward::R
    rng::RNG
    done::Bool
    box_reward::R
    box_positions::Vector{CartesianIndex{2}}
    level_number::Int
    dataset::LevelDataset
end

function SokobanUndirected(; R = Float32, file = joinpath(dirname(functionloc(GW.eval)[1]), "envs/sokoban/boxoban-levels/medium/train/000.txt"), rng = Random.GLOBAL_RNG)
    dataset = LevelDataset(file)
    height, width, _ = size(dataset.characters)

    tile_map = falses(NUM_OBJECTS, height, width)
    agent_position = CartesianIndex(1, 1)

    reward = zero(R)
    done = false
    box_reward = one(R)
    box_positions = CartesianIndex{2}[]
    level_number = 1

    env = SokobanUndirected(tile_map, agent_position, reward, rng, done, box_reward, box_positions, level_number, dataset)

    GW.reset!(env)

    return env
end

function GW.reset!(env::SokobanUndirected)
    rng = env.rng
    num_levels = size(env.dataset.characters, 3)

    new_level_number = rand(rng, 1 : num_levels)

    set_level!(env, new_level_number)

    env.reward = zero(env.reward)
    env.done = false

    return nothing
end

function GW.act!(env::SokobanUndirected, action)
    @assert action in Base.OneTo(NUM_ACTIONS) "Invalid action $(action). Action must be in Base.OneTo($(NUM_ACTIONS))"

    tile_map = env.tile_map
    box_positions = env.box_positions
    box_reward = env.box_reward

    r1 = sum(pos -> tile_map[TARGET, pos], box_positions) * box_reward
    agent_position = env.agent_position

    if action == 1
        dest = GW.move_up(agent_position)
        beyond_dest = GW.move_up(dest)
    elseif action == 2
        dest = GW.move_down(agent_position)
        beyond_dest = GW.move_down(dest)
    elseif action == 3
        dest = GW.move_left(agent_position)
        beyond_dest = GW.move_left(dest)
    else
        dest = GW.move_right(agent_position)
        beyond_dest = GW.move_right(dest)
    end

    if !tile_map[WALL, dest]
        if !tile_map[BOX, dest]
            tile_map[AGENT, agent_position] = false
            env.agent_position = dest
            tile_map[AGENT, dest] = true
        else
            if !tile_map[BOX, beyond_dest] && !tile_map[WALL, beyond_dest]
                tile_map[BOX, dest] = false
                tile_map[BOX, beyond_dest] = true

                box_idx = findfirst(==(dest), box_positions)
                box_positions[box_idx] = beyond_dest
                tile_map[AGENT, agent_position] = false
                env.agent_position = dest
                tile_map[AGENT, dest] = true
            end
        end
    end

    env.done = all(pos -> tile_map[TARGET, pos], box_positions)
    r2 = sum(pos -> tile_map[TARGET, pos], box_positions) * box_reward
    env.reward = r2 - r1

    return nothing
end

function set_level!(env::SokobanUndirected, level_number::Int)
    tile_map = env.tile_map
    characters = env.dataset.characters
    box_positions = env.box_positions

    fill!(tile_map, false)
    empty!(env.box_positions)

    env.level_number = level_number

    height, width, num_levels = size(characters)

    for j in 1 : width
        for i in 1 : height
            char = characters[i, j, level_number]
            if char === '@'
                tile_map[AGENT, i, j] = true
                env.agent_position = CartesianIndex(i, j)
            elseif char === '#'
                tile_map[WALL, i, j] = true
            elseif char === '$'
                tile_map[BOX, i, j] = true
                push!(box_positions, CartesianIndex(i, j))
            elseif char === '.'
                tile_map[TARGET, i, j] = true
            end
        end
    end

    return nothing
end

#####
##### miscellaneous
#####

GW.get_height(env::SokobanUndirected) = size(env.tile_map, 2)
GW.get_width(env::SokobanUndirected) = size(env.tile_map, 3)

GW.get_action_names(env::SokobanUndirected) = (:MOVE_UP, :MOVE_DOWN, :MOVE_LEFT, :MOVE_RIGHT)
GW.get_object_names(env::SokobanUndirected) = (:AGENT, :WALL, :BOX, :TARGET)

function GW.get_pretty_tile_map(env::SokobanUndirected, position::CartesianIndex{2})
    characters = ('☻', '█', '▒', '✖', '⋅')

    object = findfirst(@view env.tile_map[:, position])
    if isnothing(object)
        return characters[end]
    else
        return characters[object]
    end
end

function GW.get_pretty_sub_tile_map(env::SokobanUndirected, window_size, position::CartesianIndex{2})
    tile_map = env.tile_map
    agent_position = env.agent_position

    characters = ('☻', '█', '▒', '✖', '⋅')

    sub_tile_map = GW.get_sub_tile_map(tile_map, agent_position, window_size)

    object = findfirst(@view sub_tile_map[:, position])
    if isnothing(object)
        return characters[end]
    else
        return characters[object]
    end
end

function Base.show(io::IO, ::MIME"text/plain", env::SokobanUndirected)
    str = "tile_map:\n"
    str = str * GW.get_pretty_tile_map(env)
    str = str * "\nsub_tile_map:\n"
    str = str * GW.get_pretty_sub_tile_map(env, GW.get_window_size(env))
    str = str * "\nreward: $(env.reward)"
    str = str * "\ndone: $(env.done)"
    str = str * "\nlevel_number: $(env.level_number)"
    str = str * "\naction_names: $(GW.get_action_names(env))"
    str = str * "\nobject_names: $(GW.get_object_names(env))"
    print(io, str)
    return nothing
end

GW.get_action_keys(env::SokobanUndirected) = ('w', 's', 'a', 'd')

#####
##### RLBase API
#####

RLBase.StateStyle(env::GW.RLBaseEnv{E}) where {E <: SokobanUndirected} = RLBase.InternalState{Any}()
RLBase.state_space(env::GW.RLBaseEnv{E}, ::RLBase.InternalState) where {E <: SokobanUndirected} = nothing
RLBase.state(env::GW.RLBaseEnv{E}, ::RLBase.InternalState) where {E <: SokobanUndirected} = env.env.tile_map

RLBase.reset!(env::GW.RLBaseEnv{E}) where {E <: SokobanUndirected} = GW.reset!(env.env)

RLBase.action_space(env::GW.RLBaseEnv{E}) where {E <: SokobanUndirected} = Base.OneTo(NUM_ACTIONS)
(env::GW.RLBaseEnv{E})(action) where {E <: SokobanUndirected} = GW.act!(env.env, action)

RLBase.reward(env::GW.RLBaseEnv{E}) where {E <: SokobanUndirected} = env.env.reward
RLBase.is_terminated(env::GW.RLBaseEnv{E}) where {E <: SokobanUndirected} = env.env.done

end # module


# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/AsterReader.jl/blob/master/LICENSE

using FEMBase, AsterReader, Test
using AsterReader: RMEDFile, aster_read_data

datadir = first(splitext(basename(@__FILE__)))

@testset "read nodal field from result rmed file" begin
    rmedfile = joinpath(datadir, "rings.rmed")
    rmed = RMEDFile(rmedfile)
    temp = aster_read_data(rmed, "TEMP")
    @test isapprox(temp[15], 1.0)
    @test isapprox(temp[95], 2.0)
end


"""
    HorizontalStructure

Horizontal memory structure. Decomposes stochastic program by scenario.

"""
struct HorizontalStructure{N, M, SP <: NTuple{M, AbstractScenarioProblems}} <: AbstractBlockStructure{N}
    decisions::NTuple{N, Decisions}
    scenarioproblems::SP
    constraint_map::Dict{Tuple{CI, Int}, CI}

    function HorizontalStructure(decisions::NTuple{N, Decisions}, scenarioproblems::NTuple{M,AbstractScenarioProblems}) where {N, M}
        M == N - 1 || error("Inconsistent number of stages $N and number of scenario types $M")
        SP = typeof(scenarioproblems)
        return new{N,M,SP}(decisions, scenarioproblems, Dict{Tuple{CI, Int}, CI}())
    end
end

function StochasticStructure(decisions::NTuple{N, Decisions}, scenario_types::ScenarioTypes{M}, instantiation::Union{Horizontal, DistributedHorizontal}) where {N, M}
    scenarioproblems = ntuple(Val(M)) do i
        ScenarioProblems(scenario_types[i], instantiation)
    end
    return HorizontalStructure(decisions, scenarioproblems)
end

function StochasticStructure(decisions::NTuple{N, Decisions}, scenarios::NTuple{M, Vector{<:AbstractScenario}}, instantiation::Union{Horizontal, DistributedHorizontal}) where {N, M}
    scenarioproblems = ntuple(Val(M)) do i
        ScenarioProblems(scenarios[i], instantiation)
    end
    return HorizontalStructure(decisions, scenarioproblems)
end

# Base overloads #
# ========================== #
function Base.print(io::IO, structure::HorizontalStructure{2})
    print(io, "Horizontal scenario problems \n")
    print(io, "============== \n")
    for (id, subproblem) in enumerate(subproblems(structure, 2))
        @printf(io, "Subproblem %d (p = %.2f):\n", id, probability(scenario(structure, 2, id)))
        print(io, subproblem)
        print(io, "\n")
    end
end

# MOI #
# ========================== #
function MOI.get(structure::HorizontalStructure, attr::MOI.AbstractModelAttribute)
    if attr isa Union{MOI.ObjectiveFunctionType, MOI.ObjectiveSense}
        # Should be the same in all subproblems, query the first
        return MOI.get(structure, ScenarioDependentModelAttribute(2, 1, attr))
    end
    error("The horizontal structure is completely decomposed into subproblems. All model attributes are scenario dependent.")
end
function MOI.get(structure::HorizontalStructure, attr::MOI.AbstractVariableAttribute, index::MOI.VariableIndex)
    error("The horizontal structure is completely decomposed into subproblems. All model attributes are scenario dependent.")
end
function MOI.get(structure::HorizontalStructure, attr::MOI.AbstractConstraintAttribute, cindex::MOI.ConstraintIndex)
    error("The horizontal structure is completely decomposed into subproblems. All model attributes are scenario dependent.")
end
function MOI.get(structure::HorizontalStructure, attr::ScenarioDependentModelAttribute)
    n = num_scenarios(structure, attr.stage)
    1 <= attr.scenario_index <= n || error("Scenario index $attr.scenario_index not in range 1 to $n.")
    return MOI.get(scenarioproblems(structure, attr.stage), attr)
end
function MOI.get(structure::HorizontalStructure, attr::ScenarioDependentVariableAttribute, index::MOI.VariableIndex)
    n = num_scenarios(structure, attr.stage)
    1 <= attr.scenario_index <= n || error("Scenario index $attr.scenario_index not in range 1 to $n.")
    return MOI.get(scenarioproblems(structure, attr.stage), attr, index)
end
function MOI.get(structure::HorizontalStructure, attr::ScenarioDependentConstraintAttribute, ci::MOI.ConstraintIndex)
    n = num_scenarios(structure, attr.stage)
    1 <= attr.scenario_index <= n || error("Scenario index $attr.scenario_index not in range 1 to $n.")
    mapped_ci = structure.constraint_map[(ci, attr.scenario_index)]
    return MOI.get(scenarioproblems(structure, attr.stage), attr, mapped_ci)
end
function MOI.get(structure::HorizontalStructure, attr::ScenarioDependentConstraintAttribute, ci::CI{F,S}) where {F <: MOI.SingleVariable, S}
    n = num_scenarios(structure, attr.stage)
    1 <= attr.scenario_index <= n || error("Scenario index $attr.scenario_index not in range 1 to $n.")
    # Do not need to map here
    return MOI.get(scenarioproblems(structure, attr.stage), attr, ci)
end

function MOI.set(structure::HorizontalStructure{2}, attr::Union{MOI.AbstractModelAttribute, MOI.Silent}, value)
    # All subproblems should be updated
    MOI.set(scenarioproblems(structure), attr, value)
end
function MOI.set(structure::HorizontalStructure{2}, attr::MOI.AbstractVariableAttribute,
                 index::MOI.VariableIndex, value)
    # All subproblems should be updated
    MOI.set(scenarioproblems(structure), attr, index, value)
    return nothing
end
function MOI.set(structure::HorizontalStructure{2}, attr::MOI.AbstractConstraintAttribute,
                 ci::MOI.ConstraintIndex, value)
    # All subproblems should be updated
    MOI.set(scenarioproblems(structure), attr, ci, value)
    return nothing
end
function MOI.set(structure::HorizontalStructure{2}, attr::ScenarioDependentModelAttribute, value)
    n = num_scenarios(structure, attr.stage)
    1 <= attr.scenario_index <= n || error("Scenario index $attr.scenario_index not in range 1 to $n.")
    MOI.set(scenarioproblems(subproblem, attr.stage), attr, value)
    return nothing
end
function MOI.set(structure::HorizontalStructure{2}, attr::ScenarioDependentVariableAttribute,
                 index::MOI.VariableIndex, value)
    n = num_scenarios(structure, attr.stage)
    1 <= attr.scenario_index <= n || error("Scenario index $attr.scenario_index not in range 1 to $n.")
    MOI.set(scenarioproblems(structure, attr.stage), attr, index, value)
    return nothing
end
function MOI.set(structure::HorizontalStructure{2}, attr::ScenarioDependentConstraintAttribute,
                 ci::MOI.ConstraintIndex, value)
    n = num_scenarios(structure, attr.stage)
    1 <= attr.scenario_index <= n || error("Scenario index $attr.scenario_index not in range 1 to $n.")
    MOI.set(scenarioproblems(structure, attr.stage), attr, ci, value)
    return nothing
end

function MOI.is_valid(structure::HorizontalStructure{2}, index::MOI.VariableIndex, stage::Integer)
    stage == 1 || error("No scenario index specified.")
    # First-stage decision should be valid in all subproblems
    return all(MOI.is_valid(scenarioproblems(structure), index, scenario_index) for scenario_index in num_scenarios(structure))
end
function MOI.is_valid(structure::HorizontalStructure{2}, ci::MOI.ConstraintIndex, stage::Integer)
    stage == 1 || error("No scenario index specified.")
    # First-stage decision constraint should be valid in all subproblems
    return all(MOI.is_valid(scenarioproblems(structure), ci, scenario_index) for scenario_index in num_scenarios(structure))
end
function MOI.is_valid(structure::HorizontalStructure{N}, ci::MOI.ConstraintIndex, stage::Integer, scenario_index::Integer) where N
    1 <= stage <= N || error("Stage $stage not in range 1 to $N.")
    stage > 1 || error("There are no scenarios in the first stage.")
    n = num_scenarios(structure, stage)
    1 <= scenario_index <= n || error("Scenario index $scenario_index not in range 1 to $n.")
    if !haskey(structure.constraint_map, (ci, scenario_index))
        return false
    end
    mapped_ci = structure.constraint_map[(ci, scenario_index)]
    return MOI.is_valid(scenarioproblems(structure, stage), mapped_ci, scenario_index)
end

function MOI.add_constraint(structure::HorizontalStructure{2}, f::SingleDecision, s::MOI.AbstractSet)
    # Constraints should be added to every subproblem
    for scenario_index in 1:num_scenarios(structure)
        MOI.add_constraint(scenarioproblems(structure), f, s, scenario_index)
    end
    return nothing
end
function MOI.add_constraint(structure::HorizontalStructure{2}, f::SingleDecision, s::MOI.AbstractSet, stage::Integer, scenario_index::Integer)
    stage == 1 && error("There are no scenarios in the first stage.")
    n = num_scenarios(structure, 2)
    1 <= scenario_index <= n || error("Scenario index $scenario_index not in range 1 to $n.")
    new_ci = MOI.add_constraint(scenarioproblems(structure), f, s, scenario_index)
    # Add to map
    ci = CI{SingleDecision,typeof(s)}(f.decision.value)
    structure.constraint_map[(ci, scenario_index)] = new_ci
    return nothing
end

function MOI.delete(structure::HorizontalStructure{2}, index::MOI.VariableIndex, stage::Integer)
    stage == 1 || error("No scenario index specified.")
    # First-stage decision should be removed from every subproblem
    for scenario_index in 1:num_scenarios(structure)
        MOI.delete(scenarioproblems(structure), index, scenario_index)
    end
    return nothing
end
function MOI.delete(structure::HorizontalStructure{2}, indices::Vector{MOI.VariableIndex}, stage::Integer)
    stage == 1 || error("No scenario index specified.")
    # First-stage decision should be removed from every subproblem
    for scenario_index in 1:num_scenarios(structure)
        MOI.delete(scenarioproblems(structure), indices, scenario_index)
    end
    return nothing
end
function MOI.delete(structure::HorizontalStructure{2}, ci::MOI.ConstraintIndex, stage::Integer)
    stage == 1 || error("No scenario index specified.")
    # First-stage decision constraints should be removed from every subproblem
    for scenario_index in 1:num_scenarios(structure)
        MOI.delete(scenarioproblems(structure), ci, scenario_index)
    end
    return nothing
end
function MOI.delete(structure::HorizontalStructure{2}, cis::Vector{<:MOI.ConstraintIndex}, stage::Integer)
    stage == 1 || error("No scenario index specified.")
    # First-stage decision constraints should be removed from every subproblem
    for scenario_index in 1:num_scenarios(structure)
        MOI.delete(scenarioproblems(structure), cis, scenario_index)
    end
    return nothing
end
function MOI.delete(structure::HorizontalStructure{N}, ci::MOI.ConstraintIndex, stage::Integer, scenario_index::Integer) where N
    1 <= stage <= N || error("Stage $stage not in range 1 to $N.")
    stage > 1 || error("There are no scenarios in the first stage.")
    n = num_scenarios(structure, stage)
    1 <= scenario_index <= n || error("Scenario index $scenario_index not in range 1 to $n.")
    mapped_ci = structure.constraint_map[(ci, scenario_index)]
    MOI.delete(scenarioproblems(structure, stage), mapped_ci, scenario_index)
    return nothing
end
function MOI.delete(structure::HorizontalStructure{N}, cis::Vector{<:MOI.ConstraintIndex}, stage::Integer, scenario_index::Integer) where N
    1 <= stage <= N || error("Stage $stage not in range 1 to $N.")
    stage > 1 || error("There are no scenarios in the first stage.")
    n = num_scenarios(structure, stage)
    1 <= scenario_index <= n || error("Scenario index $scenario_index not in range 1 to $n.")
    mapped_cis = map(ci -> structure.constraint_map[(ci, scenario_index)], cis)
    MOI.delete(scenarioproblems(structure, stage), mapped_cis, scenario_index)
    return nothing
end

# JuMP #
# ========================== #
function JuMP.fix(structure::HorizontalStructure{2}, index::MOI.VariableIndex, stage::Integer, val::Number)
    d = decision(structure, index, stage)
    if state(d) == NotTaken
        # Update state
        d.state = Taken
        # Update value
        d.value = val
    else
        # Just update value
        d.value = val
    end
    update_decision_state!(scenarioproblems(structure), index, Taken)
    return nothing
end
function JuMP.fix(structure::HorizontalStructure{N}, index::MOI.VariableIndex, stage::Integer, scenario_index::Integer, val::Number) where N
    1 <= stage <= N || error("Stage $stage not in range 1 to $N.")
    n = num_scenarios(structure, stage)
    1 <= scenario_index <= n || error("Scenario index $scenario_index not in range 1 to $n.")
    fix(scenarioproblems(structure, stage), index, scenario_index, val)
    return nothing
end
function JuMP.unfix(structure::HorizontalStructure{2}, index::MOI.VariableIndex, stage::Integer)
    d = decision(structure, index, stage)
    if state(d) == NotTaken
        # Nothing to do, just return
        return nothing
    end
    # Update state
    d.state = NotTaken
    update_decision_state!(scenarioproblems(structure), index, NotTaken)
    return nothing
end
function JuMP.unfix(structure::HorizontalStructure{N}, index::MOI.VariableIndex, stage::Integer, scenario_index::Integer) where N
    1 <= stage <= N || error("Stage $stage not in range 1 to $N.")
    n = num_scenarios(structure, stage)
    1 <= scenario_index <= n || error("Scenario index $scenario_index not in range 1 to $n.")
    unfix(scenarioproblems(structure, stage), index, scenario_index)
    return nothing
end

function JuMP.objective_function_type(structure::HorizontalStructure)
    error("The horizontal structure is completely decomposed into subproblems. All model attributes are scenario dependent.")
end

function JuMP.objective_function(structure::HorizontalStructure, FunType::Type{<:AbstractJuMPScalar})
    error("The horizontal structure is completely decomposed into subproblems. All model attributes are scenario dependent.")
end

function JuMP.objective_function(structure::HorizontalStructure, proxy::JuMP.Model, stage::Integer, FunType::Type{<:AbstractJuMPScalar})
    return objective_function(proxy, FunType)
end

function JuMP._moi_optimizer_index(structure::HorizontalStructure, ci::CI{F,S}, scenario_index::Integer) where {F,S}
    if haskey(structure.constraint_map, (ci, scenario_index))
        ci = CI{F,S}(structure.constraint_map[(ci, scenario_index)].value)
        return JuMP._moi_optimizer_index(scenarioproblems(structure), ci, scenario_index)
    end
    return JuMP._moi_optimizer_index(scenarioproblems(structure), ci, scenario_index)
end

function JuMP.set_objective_coefficient(structure::HorizontalStructure{2}, index::VI, var_stage::Integer, stage::Integer, coeff::Real)
    if var_stage == 1
        if stage == 1
            # Modification should be applied in every subproblem
            for scenario_index in 1:num_scenarios(structure)
                set_objective_coefficient(scenarioproblems(structure), index, scenario_index, coeff)
            end
        else
            error("The horizontal structure is completely decomposed into subproblems. Can only modify first-stage part of objective for first-stage.")
        end
    else
        error("Decision is scenario dependent, consider `set_objective_coefficient(sp, dvar, stage, scenario_index, coeff)`.")
    end
    return nothing
end
function JuMP.set_objective_coefficient(structure::HorizontalStructure{N}, index::VI, var_stage::Integer, stage::Integer, scenario_index::Integer, coeff::Real) where N
    1 <= stage <= N || error("Stage $stage not in range 1 to $N.")
    stage > 1 || error("There are no scenarios in the first stage.")
    n = num_scenarios(structure, stage)
    1 <= scenario_index <= n || error("Scenario index $scenario_index not in range 1 to $n.")
    set_objective_coefficient(scenarioproblems(structure, stage), index, scenario_index, coeff)
    return nothing
end

function JuMP.set_normalized_coefficient(structure::HorizontalStructure,
                                         ci::CI{F,S},
                                         index::VI,
                                         value) where {T, F <: Union{AffineDecisionFunction{T}, QuadraticDecisionFunction{T}}, S}
    # Modification should be applied in every subproblem
    for scenario_index in 1:num_scenarios(structure)
        set_normalized_coefficient(scenarioproblems(structure), ci, index, scenario_index, value)
    end
    return nothing
end
function JuMP.set_normalized_coefficient(structure::HorizontalStructure{N},
                                         ci::CI{F,S},
                                         index::VI,
                                         stage::Integer,
                                         scenario_index::Integer,
                                         value) where {N, T, F <: Union{AffineDecisionFunction{T}, QuadraticDecisionFunction{T}}, S}
    1 <= stage <= N || error("Stage $stage not in range 1 to $N.")
    stage > 1 || error("There are no scenarios in the first stage.")
    n = num_scenarios(structure, stage)
    1 <= scenario_index <= n || error("Scenario index $scenario_index not in range 1 to $n.")
    mapped_ci = structure.constraint_map[(ci, scenario_index)]
    set_normalized_coefficient(scenarioproblems(structure, stage), mapped_ci, index, scenario_index, value)
    return nothing
end

function JuMP.normalized_coefficient(structure::HorizontalStructure{N},
                                     ci::CI{F,S},
                                     index::VI,
                                     stage::Integer,
                                     scenario_index::Integer) where {N, T, F <: Union{AffineDecisionFunction{T}, QuadraticDecisionFunction{T}}, S}
    1 <= stage <= N || error("Stage $stage not in range 1 to $N.")
    stage > 1 || error("There are no scenarios in the first stage.")
    n = num_scenarios(structure, stage)
    1 <= scenario_index <= n || error("Scenario index $scenario_index not in range 1 to $n.")
    mapped_ci = structure.constraint_map[(ci, scenario_index)]
    return normalized_coefficient(scenarioproblems(structure, stage), mapped_ci, index, scenario_index)
end

function JuMP.set_normalized_rhs(structure::HorizontalStructure,
                                 ci::CI{F,S},
                                 value) where {T,
                                               F <: Union{AffineDecisionFunction{T}, QuadraticDecisionFunction{T}},
                                               S <: MOIU.ScalarLinearSet{T}}
    # Modification should be applied in every subproblem
    for scenario_index in 1:num_scenarios(structure)
        set_normalized_rhs(scenarioproblems(structure), ci, scenario_index, value)
    end
    return nothing
end
function JuMP.set_normalized_rhs(structure::HorizontalStructure{N},
                                 ci::CI{F,S},
                                 stage::Integer,
                                 scenario_index::Integer,
                                 value) where {N,
                                               T,
                                               F <: Union{AffineDecisionFunction{T}, QuadraticDecisionFunction{T}},
                                               S <: MOIU.ScalarLinearSet{T}}
    1 <= stage <= N || error("Stage $stage not in range 1 to $N.")
    stage > 1 || error("There are no scenarios in the first stage.")
    n = num_scenarios(structure, stage)
    1 <= scenario_index <= n || error("Scenario index $scenario_index not in range 1 to $n.")
    mapped_ci = structure.constraint_map[(ci, scenario_index)]
    set_normalized_rhs(scenarioproblems(structure, stage), mapped_ci, scenario_index, value)
    return nothing
end

function DecisionRef(proxy::JuMP.Model, structure::HorizontalStructure, index::VI)
    return DecisionRef(proxy, index)
end

function JuMP.jump_function(structure::HorizontalStructure{N},
                            proxy::JuMP.Model,
                            stage::Integer,
                            f::MOI.AbstractFunction) where N
    1 <= stage <= N || error("Stage $stage not in range 1 to $N.")
    return JuMP.jump_function(proxy, f)
end

# Getters #
# ========================== #
function structure_name(structure::HorizontalStructure)
    return "Horizontal"
end

function decision(structure::HorizontalStructure{N}, index::MOI.VariableIndex, stage::Integer) where N
    stage == 1 || error("No scenario index specified.")
    return decision(structure.decisions[stage], index)
end

# Setters #
# ========================== #
function update_known_decisions!(structure::HorizontalStructure{2})
    # Modification should be applied in every subproblem
    for scenario_index in 1:num_scenarios(structure)
        update_known_decisions!(scenarioproblems(structure), scenario_index)
    end
    return nothing
end

function untake_decisions!(structure::HorizontalStructure{2,1,NTuple{1,SP}}) where SP <: ScenarioProblems
    if untake_decisions!(structure.decisions[1])
        update_decisions!(scenarioproblems(structure), DecisionsStateChange())
    end
    return nothing
end
function untake_decisions!(structure::HorizontalStructure{2,1,NTuple{1,SP}}) where SP <: DistributedScenarioProblems
    sp = scenarioproblems(structure)
    @sync begin
        for (i,w) in enumerate(workers())
            @async remotecall_fetch(
                w, sp[w-1], sp.decisions[w-1]) do sp, d
                    if untake_decisions!(fetch(d))
                        update_decisions!(fetch(sp), DecisionsStateChange())
                    end
                end
        end
    end
    return nothing
end


export RanklibRanker

@with_kw mutable struct RanklibRanker <: BaseEstimator
    ranklib::Vector{UInt8} = UInt8[]
    ranker::Int = 0
    metric2t::String = "MAP"
end

is_classifier(::RanklibRanker) = false
is_ranker(::RanklibRanker) = true

function gridparams(m::RanklibRanker)
    grid = [
        "ranker" => [0:4; 6:8],
        "metric2t" =>
        ["MAP"; ["$s@$k" for k in [20, 50, 100]
        for s in split("NDCG DCG P RR ERR")]]
    ]
    filter(gridparams(grid)) do d
        d["metric2t"] == "MAP" || d["ranker"] ∈ [3, 4, 6]
    end
end

function fit!(m::RanklibRanker, x, y, w = nothing; group = nothing, columns = nothing)
    columns = something(columns, string.(1:size(x, 1)))
    if isnothing(group) && ndims(x) == 3
        group = repeat([size(x, 2)], size(x, 3))
    end
    @unpack ranker, metric2t = m
    dst = to_svm(x, y, w, group = group)
    run(`$RANKLIB -train $dst -save ranklib -ranker $ranker -metric2t $metric2t`)
    m.ranklib = read("ranklib")
    foreach(rm, ["ranklib", dst])
    BSON.bson("model.bson", model = m)
    return m
end

function predict(m::RanklibRanker, x)
    @unpack ranklib = m
    dst = to_svm(x)
    write("ranklib", ranklib)
    run(`$RANKLIB -load ranklib -rank $dst -score preds`)
    ŷ = readdlm("preds", Float32)[:, 3]
    foreach(rm, ["ranklib", "preds", dst])
    return ŷ
end


"""
    ArcStandard()

Transition system for for Arc-Standard dependency parsing.

# Transitions

| Transition  | Definition                                         |
|:----------- |:-------------------------------------------------- |
| LeftArc(l)  | (σ\\|s1\\|s0, β, A) → (σ\\|s0, β, A ∪ (s0, l, s1)) |
| RightArc(l) | (σ\\|s, b\\|β, A) → (σ, b\\|β, A ∪ (b, l, s))      |
| Shift	      | (σ,  b\\|β, A) → (σ\\|b, β, A)                     |

# Preconditions

| Transition  | Condition                          |
|:----------- |:---------------------------------- |
| LeftArc(l)  | ¬[s1 = 0], ¬∃k∃l'[(k, l', s1) ϵ A] |
| RightArc(l) | ¬∃k∃l'[(k, l', s0) ϵ A]            |

See [Nivre 2004](https://www.aclweb.org/anthology/W04-0308.pdf).
"""
struct ArcStandard <: AbstractTransitionSystem end

initconfig(s::ArcStandard, graph::DependencyTree) = ArcStandardConfig(graph)
initconfig(s::ArcStandard, words) = ArcStandardConfig(words)

projective_only(::ArcStandard) = true

transition_space(::ArcStandard, labels=[]) =
    isempty(labels) ? [LeftArc(), RightArc(), Shift()] :
    [LeftArc.(labels)..., RightArc.(labels)..., Shift()]

struct ArcStandardConfig <: AbstractParserConfiguration
    stack::Vector{Int}
    buffer::Vector{Int}
    A::Vector{Token}
end

ArcStandardConfig(sentence) = stack_buffer_config(ArcStandardConfig, sentence)

buffer(cfg::ArcStandardConfig) = cfg.buffer
stack(cfg::ArcStandardConfig)  = cfg.stack
tokens(cfg::ArcStandardConfig) = cfg.A

function Base.show(io::IO, cfg::ArcStandardConfig)
    print(io, "$(typeof(cfg))($(stack(cfg)),$(buffer(cfg)))")
    for (i, t) in enumerate(tokens(cfg))
        print("(",join((i, t.form, t.head), ","), ")")
    end
end

function apply_transition(f, cfg::ArcStandardConfig, a...; k...)
    σ, β, A = f(cfg.stack, cfg.buffer, cfg.A, a...; k...)
    return ArcStandardConfig(σ, β, A)
end

leftarc(cfg::ArcStandardConfig, args...; kwargs...) =
    apply_transition(leftarc_reduce2, cfg, args...; kwargs...)

rightarc(cfg::ArcStandardConfig, args...; kwargs...) =
    apply_transition(rightarc_reduce, cfg, args...; kwargs...)

shift(cfg::ArcStandardConfig) =
    apply_transition(shift, cfg)

isfinal(cfg::ArcStandardConfig) =
    length(stack(cfg)) == 1 && stack(cfg)[1] == 0 && length(buffer(cfg)) == 0

"""
    static_oracle(cfg, gold_tree)

Static oracle function for arc-standard dependency parsing.
"""
function static_oracle(cfg::ArcStandardConfig, gold_tree, arc=untyped)
    l = i -> arc(gold_tree[i])
    s = stack(cfg)
    if length(s) >= 2
        s1, s0 = s[end-1], s[end]
        if has_arc(gold_tree, s0, s1)
            return LeftArc(l(s1)...)
        elseif has_arc(gold_tree, s1, s0)
            if !any(k -> (k in s || k in buffer(cfg)), deps(gold_tree, s0))
                return RightArc(l(s0)...)
            end
        end
    end
    return Shift()
end

function is_possible(::LeftArc, cfg::ArcStandardConfig)
    if stacklength(cfg) >= 2
        s, s1, s0 = popstack(cfg, 2)
        return s1 != 0 && !has_head(token(cfg, s1))
    else
        return false
    end
end

is_possible(::RightArc, cfg::ArcStandardConfig) =
    stacklength(cfg) > 1 && !has_head(token(cfg, last(stack(cfg))))

is_possible(::Shift, cfg::ArcStandardConfig) = stacklength(cfg) > 0

function possible_transitions(cfg::ArcStandardConfig, arc=untyped)
    l = i -> arc(token(cfg, i))
    if stacklength(cfg) >= 2
        σ, s1, s0 = popstack(cfg, 2)
        transitions = [LeftArc(l(s1)...), RightArc(l(s0)...), Shift()]
        return filter(t -> is_possible(t, cfg), transitions)
    else
        return filter(t -> is_possible(t, cfg), TransitionOperator[Shift()])
    end
end

possible_transitions(cfg::ArcStandardConfig, gold_tree::DependencyTree, arc=untyped) =
    possible_transitions(cfg, arc)

==(cfg1::ArcStandardConfig, cfg2::ArcStandardConfig) =
    cfg1.stack == cfg2.stack && cfg1.buffer == cfg2.buffer && cfg1.A == cfg2.A


"""
Create a 1Q GST sequence for a csv file of single qubit Clifford numbers

create_1Q_GST_seqs(file, q; Id_length=50e-9, num_cals=500)

file	 : path to csv file
q		 : name of qubit in scope
Id_length : length of the null {} pulse in seconds this is typically 0
num_cals	 : number of calibration sequences for 0 and 1

"""
function create_1Q_GST_seqs(file, q; Id_length=0, num_cals=500)

	# create pulses once in a library
	m = MEAS(q)
	pulse_lib = [DiAC(q,x) for x in 1:24]
	insert!(pulse_lib, 1, QGL.PulseBlock(Id(q,Id_length)))

	seqs = Vector{Vector{QGL.SequenceEntry}}()
	f = open(file, "r")
	for ln in eachline(f)
		# convert from string to Int and 0-indexed to 1-indexed
		pulse_nums = map(x -> parse(Int, x) + 1, split(ln, ','))
		seq = Vector{QGL.SequenceEntry}(length(pulse_nums) + 1)
		map!(x -> pulse_lib[x], seq, pulse_nums)
		seq[end] = m
		push!(seqs, seq)
	end
	cals = cal_seqs((q,),num_repeats=num_cals)
	seqs = vcat(seqs, cals)
	return seqs
end
"""
Create a 1Q GST sequence for a csv file of single qubit Clifford numbers
using diatomic pulses

create_1Q_GST_seqs(file::Array{String,1}, q; Id_length=50e-9, num_cals=500)

file	 : a string array with the paths to multiple csv files
q		 : name of qubit in scope
Id_length : length of the null {} pulse in seconds this is typically 0
num_cals	 : number of calibration sequences for 0 and 1
"""
function create_1Q_GST_seqs(file::Array{String, 1}, q; Id_length=0,
	num_cals=500)

	# create pulses once in a library
	m = MEAS(q)
	pulse_lib = [DiAC(q,x) for x in 1:24]
	insert!(pulse_lib, 1, QGL.PulseBlock(Id(q,Id_length)))

	seqs = Vector{Vector{QGL.SequenceEntry}}()
	cals = cal_seqs((q,),num_repeats=num_cals)
	for fname in file
		f = open(fname, "r")
		for ln in eachline(f)
			# convert from string to Int and 0-indexed to 1-indexed
			pulse_nums = map(x -> parse(Int, x) + 1, split(ln, ','))
			seq = Vector{QGL.SequenceEntry}(length(pulse_nums) + 1)
			map!(x -> pulse_lib[x], seq, pulse_nums)
			seq[end] = m
			push!(seqs, seq)
		end
		seqs = vcat(seqs, cals)
	end
	return seqs
end
"""
Create a 1Q GST sequence for a csv file of single qubit Clifford numbers using
diatomic pulses.  The experimental sequence specified by the csv file is
repeated 10 times including cals.

create_1Q_GST10_seqs(file, q; Id_length=50e-9, num_cals=500)

file	 : path to csv file
q		 : name of qubit in scope
Id_length : length of the null {} pulse in seconds this is typically 0
num_cals	 : number of calibration sequences for 0 and 1
"""
function create_1Q_GST10_seqs(file, q; Id_length=0, num_cals=500)

	# create pulses once in a library
	m = MEAS(q)
	pulse_lib = [DiAC(q,x) for x in 1:24]
	insert!(pulse_lib, 1, QGL.PulseBlock(Id(q,Id_length)))
	seqs = Vector{Vector{QGL.SequenceEntry}}()
	f = open(file, "r")
	for ln in eachline(f)
		# convert from string to Int and 0-indexed to 1-indexed
		pulse_nums = map(x -> parse(Int, x) + 1, split(ln, ','))
		seq = Vector{QGL.SequenceEntry}(length(pulse_nums) + 1)
		map!(x -> pulse_lib[x], seq, pulse_nums)
		seq[end] = m
		push!(seqs, seq)
	end
	cals = cal_seqs((q,),num_repeats=num_cals)
	seqs = vcat(seqs, cals)

	# x10
	seqs = repeat(seqs, outer=[10])

	return seqs
end


(cnet::CachedNet)(σ::LUState) =
    logψ(cnet, σ)

logψ(cnet::CachedNet, σ::LUState{<:DoubleState}) = begin
    σ_r = row(state(σ))
    σ_c = col(state(σ))

    return logψ(cnet.net, cnet.cache, lut(σ),
                config(raw_state(σ_r)), config(raw_state(σ_c)),
                changes(σ_r), changes(σ_c))
end

logψ_and_∇logψ(cnet::CachedNet, σ::LUState) = begin
   # @warn "Inefficient calling logψ_and_∇logψ for cachedNet"
    ∇lnψ = grad_cache(cnet)
    lnψ, ∇lnψ = logψ_and_∇logψ!(∇lnψ, cnet, σ)
    return lnψ, ∇lnψ
end

logψ_and_∇logψ!(∇lnψ, cnet::CachedNet, σ::LUState) = begin
    σ_r = row(state(σ))
    σ_c = col(state(σ))

    lnψ = logψ_and_∇logψ!(∇lnψ, cnet.net, cnet.cache, lut(σ),
                          config(raw_state(σ_r)), config(raw_state(σ_c)),
                          changes(σ_r), changes(σ_c))
    return lnψ, ∇lnψ
end

logψ_Δ(cnet::CachedNet, σ::LUState{<:DoubleState}) = begin
    σ_r = row(state(σ))
    σ_c = col(state(σ))
    return Δ_logψ(cnet.net, cnet.cache, lut(σ),
                  config(raw_state(σ_r)), config(raw_state(σ_c)),
                  changes(σ_r), changes(σ_c))
end

logψ_Δ(cnet::CachedNet, σ::LUState{<:DoubleState}, changes_r, changes_c) = begin
    σ_r = row(state(σ))
    σ_c = col(state(σ))
    return Δ_logψ(cnet.net, cnet.cache, lut(σ),
                  config(raw_state(σ_r)), config(raw_state(σ_c)),
                  changes_r, changes_c)
end

function Δ_logψ_and_∇logψ(cnet::CachedNet, σ::LUState, args...)
    ∇lnψ = grad_cache(cnet)
    return Δ_logψ_and_∇logψ!(∇lnψ, cnet, σ, args...)
end

Δ_logψ_and_∇logψ!(∇lnψ, cnet::CachedNet, σ::LUState{<:DoubleState}) = begin
    σ_r  = row(state(σ))
    σ_c  = col(state(σ))
    Δlnψ = Δ_logψ_and_∇logψ!(∇lnψ,
                  cnet.net, cnet.cache, lut(σ),
                  config(raw_state(σ_r)), config(raw_state(σ_c)),
                  changes(σ_r), changes(σ_c))
    return Δlnψ, ∇lnψ
end

Δ_logψ_and_∇logψ!(∇lnψ, cnet::CachedNet, σ::LUState{<:DoubleState}, changes_r, changes_c) = begin
    σ_r  = row(state(σ))
    σ_c  = col(state(σ))
    Δlnψ = Δ_logψ_and_∇logψ!(∇lnψ,
                  cnet.net, cnet.cache, lut(σ),
                  config(raw_state(σ_r)), config(raw_state(σ_c)),
                  changes_r, changes_c)
    return Δlnψ, ∇lnψ
end

prepare_lut!(σ::LUState, cnet::CachedNet) =
    set_lookup!(lut(σ), cnet.net, cnet.cache, raw_config(state(σ))...)
    #set_lookup!(lut(σ), cnet.net, cnet.cache, config(state(σ))...)

apply_lut_updates!(σ::LUState{<:DoubleState}, cnet::CachedNet) = begin
    σ_r = row(state(σ))
    σ_c = col(state(σ))
    update_lookup!(lut(σ), cnet.net, cnet.cache,
        config(raw_state(σ_r)), config(raw_state(σ_c)),
        changes(σ_r), changes(σ_c))
end

update_lookup!(σ::LUState, cnet::CachedNet) = begin
    # TODO Check if this is correct. I could just rename this, no?
    update_lut!(σ, cnet)
end


#####
##### Navier-Stokes and tracer advection equations
#####

""" Store previous value of the source term and calculate current source term. """
function calculate_interior_tendency_contributions!(G, arch, grid, coriolis, buoyancy, surface_waves, closure,
                                                    U, C, pHY′, K, F, clock)

    # Manually choose thread-block layout here as it's ~20% faster.
    # See: https://github.com/climate-machine/Oceananigans.jl/pull/308
    Nx, Ny, Nz = grid.Nx, grid.Ny, grid.Nz

    if Nx == 1
        Tx, Ty = 1, min(256, Ny)
        Bx, By, Bz = Tx, floor(Int, Ny/Ty), Nz
    elseif Ny == 1
        Tx, Ty = min(256, Nx), 1
        Bx, By, Bz = floor(Int, Nx/Tx), Ty, Nz
    else
        Tx, Ty = 16, 16
        Bx, By, Bz = floor(Int, Nx/Tx), floor(Int, Ny/Ty), Nz
    end

    @launch(device(arch), threads=(Tx, Ty), blocks=(Bx, By, Bz),
            calculate_Gu!(G.u, grid, coriolis, surface_waves, closure, U, C, K, F, pHY′, clock))

    @launch(device(arch), threads=(Tx, Ty), blocks=(Bx, By, Bz),
            calculate_Gv!(G.v, grid, coriolis, surface_waves, closure, U, C, K, F, pHY′, clock))

    @launch(device(arch), threads=(Tx, Ty), blocks=(Bx, By, Bz),
            calculate_Gw!(G.w, grid, coriolis, surface_waves, closure, U, C, K, F, clock))

    for tracer_index in 1:length(C)
        @inbounds Gc = G[tracer_index+3]
        @inbounds Fc = F[tracer_index+3]
        @inbounds  c = C[tracer_index]

        @launch(device(arch), threads=(Tx, Ty), blocks=(Bx, By, Bz),
                calculate_Gc!(Gc, grid, c, Val(tracer_index), closure, buoyancy, U, C, K, Fc, clock))
    end

    return nothing
end

"""
    calculate_velocity_tendencies_on_boundaries!(tendency_calculation_args...)

Calculate the velocity tendencies *on* east, north, and top boundaries, when the
x-, y-, or z- directions have a `Bounded` topology.
"""
function calculate_velocity_tendencies_on_boundaries!(tendency_calculation_args...)
    calculate_east_boundary_Gu!(tendency_calculation_args...)
    calculate_north_boundary_Gv!(tendency_calculation_args...)
    calculate_top_boundary_Gw!(tendency_calculation_args...)
    return nothing
end

# Fallbacks for non-bounded topologies in x-, y-, and z-directions
@inline calculate_east_boundary_Gu!(args...) = nothing
@inline calculate_north_boundary_Gv!(args...) = nothing
@inline calculate_top_boundary_Gw!(args...) = nothing

"""
    calculate_east_boundary_Gu!(G, arch, grid::AbstractGrid{FT, <:Bounded},
                                coriolis, buoyancy, surface_waves, closure,
                                U, C, pHY′, K, F, clock) where FT

Calculate `Gu` on east boundaries when the x-direction has `Bounded` topology.
"""
function calculate_east_boundary_Gu!(G, arch, grid::AbstractGrid{FT, <:Bounded},
                                     coriolis, buoyancy, surface_waves, closure,
                                     U, C, pHY′, K, F, clock) where FT

    @launch(device(arch), config=launch_config(grid, :yz),
            _calculate_east_boundary_Gu!(G.u, grid, coriolis, surface_waves,
                                         closure, U, C, K, F, pHY′, clock))

    return nothing
end

"""
    calculate_north_boundary_Gu!(G, arch, grid::AbstractGrid{FT, <:Bounded},
                                 coriolis, buoyancy, surface_waves, closure,
                                 U, C, pHY′, K, F, clock) where FT

Calculate `Gv` on north boundaries when the y-direction has `Bounded` topology.
"""
function calculate_north_boundary_Gv!(G, arch, grid::AbstractGrid{FT, TX, <:Bounded},
                                      coriolis, buoyancy, surface_waves, closure,
                                      U, C, pHY′, K, F, clock) where {FT, TX}

    @launch(device(arch), config=launch_config(grid, :xz),
            _calculate_north_boundary_Gv!(G.v, grid, coriolis, surface_waves,
                                          closure, U, C, K, F, pHY′, clock))

    return nothing
end

"""
    calculate_top_boundary_Gw!(G, arch, grid::AbstractGrid{FT, <:Bounded},
                               coriolis, buoyancy, surface_waves, closure,
                               U, C, pHY′, K, F, clock) where FT

Calculate `Gw` on top boundaries when the z-direction has `Bounded` topology.
"""
function calculate_top_boundary_Gw!(G, arch, grid::AbstractGrid{FT, TX, TY, <:Bounded},
                                    coriolis, buoyancy, surface_waves, closure,
                                    U, C, pHY′, K, F, clock) where {FT, TX, TY}

    @launch(device(arch), config=launch_config(grid, :xy),
            _calculate_top_boundary_Gw!(G.w, grid, coriolis, surface_waves,
                                        closure, U, C, K, F, clock))

    return nothing
end

#####
##### Tendency calculators for u-velocity, aka x-velocity
#####

""" Calculate the right-hand-side of the u-velocity equation. """
function calculate_Gu!(Gu, grid, coriolis, surface_waves, closure, U, C, K, F, pHY′, clock)
    @loop_xyz i j k grid begin
        @inbounds Gu[i, j, k] = u_velocity_tendency(i, j, k, grid, coriolis, surface_waves,
                                                    closure, U, C, K, F, pHY′, clock)
    end
    return nothing
end

""" Calculate the right-hand-side of the u-velocity equation on the east boundary. """
function _calculate_east_boundary_Gu!(Gu, grid, coriolis, surface_waves,
                                      closure, U, C, K, F, pHY′, clock)
    i = grid.Nx + 1
    @loop_yz j k grid begin
        @inbounds Gu[i, j, k] = u_velocity_tendency(i, j, k, grid, coriolis, surface_waves,
                                                    closure, U, C, K, F, pHY′, clock)
    end
    return nothing
end

#####
##### Tendency calculators for v-velocity
#####

""" Calculate the right-hand-side of the v-velocity equation. """
function calculate_Gv!(Gv, grid, coriolis, surface_waves, closure, U, C, K, F, pHY′, clock)
    @loop_xyz i j k grid begin
        @inbounds Gv[i, j, k] = v_velocity_tendency(i, j, k, grid, coriolis, surface_waves,
                                                    closure, U, C, K, F, pHY′, clock)
    end
    return nothing
end

""" Calculate the right-hand-side of the v-velocity equation on the north boundary. """
function _calculate_north_boundary_Gv!(Gv, grid, coriolis, surface_waves,
                                       closure, U, C, K, F, pHY′, clock)
    j = grid.Ny + 1
    @loop_xz i k grid begin
        @inbounds Gv[i, j, k] = v_velocity_tendency(i, j, k, grid, coriolis, surface_waves,
                                                    closure, U, C, K, F, pHY′, clock)
    end
    return nothing
end

#####
##### Tendency calculators for w-velocity
#####

""" Calculate the right-hand-side of the w-velocity equation. """
function calculate_Gw!(Gw, grid, coriolis, surface_waves, closure, U, C, K, F, clock)
    @loop_xyz i j k grid begin
        @inbounds Gw[i, j, k] = w_velocity_tendency(i, j, k, grid, coriolis, surface_waves,
                                                    closure, U, C, K, F, clock)
    end
    return nothing
end

""" Calculate the right-hand-side of the w-velocity equation. """
function _calculate_top_boundary_Gw!(Gw, grid, coriolis, surface_waves, closure, U, C, K, F, clock)
    k = grid.Nz + 1
    @loop_xy i j grid begin
        @inbounds Gw[i, j, k] = w_velocity_tendency(i, j, k, grid, coriolis, surface_waves,
                                                    closure, U, C, K, F, clock)
    end
    return nothing
end

#####
##### Tracer(s)
#####

""" Calculate the right-hand-side of the tracer advection-diffusion equation. """
function calculate_Gc!(Gc, grid, c, tracer_index, closure, buoyancy, U, C, K, Fc, clock)
    @loop_xyz i j k grid begin
        @inbounds Gc[i, j, k] = tracer_tendency(i, j, k, grid, c, tracer_index,
                                                closure, buoyancy, U, C, K, Fc, clock)
    end
    return nothing
end

#####
##### Boundary contributions to tendencies due to user-prescribed fluxes
#####

""" Apply boundary conditions by adding flux divergences to the right-hand-side. """
function calculate_boundary_tendency_contributions!(Gⁿ, arch, U, C, clock, state)

    # Velocity fields
    for i in 1:3
        apply_x_bcs!(Gⁿ[i], U[i], arch, clock, state)
        apply_y_bcs!(Gⁿ[i], U[i], arch, clock, state)
        apply_z_bcs!(Gⁿ[i], U[i], arch, clock, state)
    end

    # Tracer fields
    for i in 4:length(Gⁿ)
        apply_x_bcs!(Gⁿ[i], C[i-3], arch, clock, state)
        apply_y_bcs!(Gⁿ[i], C[i-3], arch, clock, state)
        apply_z_bcs!(Gⁿ[i], C[i-3], arch, clock, state)
    end

    return nothing
end

#####
##### Vertical integrals
#####

"""
Update the hydrostatic pressure perturbation pHY′. This is done by integrating
the `buoyancy_perturbation` downwards:

    `pHY′ = ∫ buoyancy_perturbation dz` from `z=0` down to `z=-Lz`
"""
function update_hydrostatic_pressure!(pHY′, grid, buoyancy, C)
    @loop_xy i j grid begin
        @inbounds pHY′[i, j, grid.Nz] = - ℑzᵃᵃᶠ(i, j, grid.Nz+1, grid, buoyancy_perturbation, buoyancy, C) * ΔzF(i, j, grid.Nz+1, grid)
        @unroll for k in grid.Nz-1 : -1 : 1
            @inbounds pHY′[i, j, k] =
                pHY′[i, j, k+1] - ℑzᵃᵃᶠ(i, j, k+1, grid, buoyancy_perturbation, buoyancy, C) * ΔzF(i, j, k+1, grid)
        end
    end
    return nothing
end

"""
Compute the vertical velocity w by integrating the continuity equation from the bottom upwards

    `w^{n+1} = -∫ [∂/∂x (u^{n+1}) + ∂/∂y (v^{n+1})] dz`
"""
function compute_w_from_continuity!(model)
    @launch(device(model.architecture), config=launch_config(model.grid, :xy),
            _compute_w_from_continuity!(datatuple(model.velocities), model.grid))
    return nothing
end

function _compute_w_from_continuity!(U, grid)
    @loop_xy i j grid begin
        # U.w[i, j, 1] = 0 is enforced via halo regions.
        @unroll for k in 2:grid.Nz
            @inbounds U.w[i, j, k] = U.w[i, j, k-1] - ΔzC(i, j, k, grid) * hdivᶜᶜᵃ(i, j, k-1, grid, U.u, U.v)
        end
    end
    return nothing
end


using CSyntax
using LinearAlgebra

@static if Sys.isapple()
    const VERSION_MAJOR = 4
    const VERSION_MINOR = 1
end

include(joinpath(@__DIR__, "glutils.jl"))
include(joinpath(@__DIR__, "camera.jl"))

# init window
width, height = 640, 480
window = startgl(width, height)

glEnable(GL_DEPTH_TEST)
glDepthFunc(GL_LESS)

# camera
camera = PerspectiveCamera()
setposition!(camera, [0.0, 0.0, 2.0])

# vertex data
points = GLfloat[-0.5, -0.5, 0.0,
                  0.5, -0.5, 0.0,
                  0.5,  0.5, 0.0,
                  0.5,  0.5, 0.0,
                 -0.5,  0.5, 0.0,
                 -0.5, -0.5, 0.0]

texcoords = GLfloat[0.0, 0.0,
                    1.0, 0.0,
                    1.0, 1.0,
                    1.0, 1.0,
                    0.0, 1.0,
                    0.0, 0.0]

# create VBO
points_vbo = GLuint(0)
@c glGenBuffers(1, &points_vbo)
glBindBuffer(GL_ARRAY_BUFFER, points_vbo)
glBufferData(GL_ARRAY_BUFFER, sizeof(points), points, GL_STATIC_DRAW)

texcoords_vbo = GLuint(0)
@c glGenBuffers(1, &texcoords_vbo)
glBindBuffer(GL_ARRAY_BUFFER, texcoords_vbo)
glBufferData(GL_ARRAY_BUFFER, sizeof(texcoords), texcoords, GL_STATIC_DRAW)

# create VAO
vao = GLuint(0)
@c glGenVertexArrays(1, &vao)
glBindVertexArray(vao)
glBindBuffer(GL_ARRAY_BUFFER, points_vbo)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, C_NULL)
glBindBuffer(GL_ARRAY_BUFFER, texcoords_vbo)
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, C_NULL)
glEnableVertexAttribArray(0)
glEnableVertexAttribArray(1)

# create shader program
vert_shader = createshader(joinpath(@__DIR__, "blending.vert"), GL_VERTEX_SHADER)
frag_shader = createshader(joinpath(@__DIR__, "blending.frag"), GL_FRAGMENT_SHADER)
shader_prog = createprogram(vert_shader, frag_shader)

model_loc = glGetUniformLocation(shader_prog, "model")
view_loc = glGetUniformLocation(shader_prog, "view")
proj_loc = glGetUniformLocation(shader_prog, "proj")
glUseProgram(shader_prog)
glUniformMatrix4fv(view_loc, 1, GL_FALSE, get_view_matrix(camera))
glUniformMatrix4fv(proj_loc, 1, GL_FALSE, get_projective_matrix(window, camera))

# load texture
glActiveTexture(GL_TEXTURE0)
tex_a = load_texture(joinpath(@__DIR__, "blob.png"))
tex_b = load_texture(joinpath(@__DIR__, "blob2.png"))

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glEnable(GL_BLEND)
# glDisable(GL_DEPTH_TEST)

# enable cull face
glEnable(GL_CULL_FACE)
glCullFace(GL_BACK)
glFrontFace(GL_CCW)
# set background color to gray
glClearColor(0.2, 0.2, 0.2, 1.0)

let
updatefps = FPSCounter()
# render
while !GLFW.WindowShouldClose(window)
    updatefps(window)
    # clear drawing surface
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glViewport(0, 0, GLFW.GetFramebufferSize(window)...)
    # drawing
    glUseProgram(shader_prog)
    glBindVertexArray(vao)

    glDepthMask(GL_FALSE)

    glBindTexture(GL_TEXTURE_2D, tex_a)
    model_matrix = Matrix{GLfloat}(I, 4, 4)
    glUniformMatrix4fv(model_loc, 1, GL_FALSE, model_matrix)
    glDrawArrays(GL_TRIANGLES, 0, 6)

    glBindTexture(GL_TEXTURE_2D, tex_b)
    model_matrix[1,4] = 0.5
    model_matrix[3,4] = 0.1
    glUniformMatrix4fv(model_loc, 1, GL_FALSE, model_matrix)
    glDrawArrays(GL_TRIANGLES, 0, 6)

    glDepthMask(GL_TRUE)

    # check and call events
    GLFW.PollEvents()
    # move camera
    updatecamera!(window, camera)
    glUseProgram(shader_prog)
    glUniformMatrix4fv(view_loc, 1, GL_FALSE, get_view_matrix(camera))
    # swap the buffers
    GLFW.SwapBuffers(window)
end
end # let

GLFW.DestroyWindow(window)