using DataDeps
using BSON

@testset "Custom layers" begin
    @testset "WeightDroppedLSTM" begin
        wd = ULMFiT.WeightDroppedLSTM(4, 5, 0.3)
        @test all(wd.init .== wd.state)
        @test size(wd.cell.Wi) == size(wd.cell.maskWi)
        @test size(wd.cell.Wh) == size(wd.cell.maskWh)
        @test wd.cell.active
        @test_throws DimensionMismatch wd(rand(5, 3))
        x = rand(Float32, 4, 3)
        h = wd(x)
        @test size(h) == size(wd.state[1]) == (5, 3)
        maski = deepcopy(wd.cell.maskWi)
        maskh = deepcopy(wd.cell.maskWh)
        ULMFiT.reset_masks!(wd)
        @test maski != wd.cell.maskWi
        @test maskh != wd.cell.maskWh
        Flux.testmode!(wd)
        @test !(wd.cell.active)
        @test length(params(wd)) == 5
    end

    @testset "AWD_LSTM" begin
        awd = ULMFiT.AWD_LSTM(4, 3, 0.3)
        @test awd.layer.cell isa ULMFiT.WeightDroppedLSTMCell
        @test length(awd.accum) == 0
        ULMFiT.set_trigger!(4, awd)
        @test awd.T == 4
        ULMFiT.asgd_step!(4, awd)
        @test length(awd.accum) == 3
        temp = deepcopy(awd.accum[1][1])
        @test temp == Tracker.data(awd.layer.cell.Wi[1])
        ULMFiT.asgd_step!(5, awd)
        temp += temp
        @test temp == Tracker.data(awd.accum[1][1])
        @test length(params(awd)) == 5
    end

    @testset "VarDrop" begin
        vd = ULMFiT.VarDrop(0.3)
        @test vd.active
        @test vd.reset
        x = rand(10, 10)
        @test size(vd(x)) == size(x) == size(vd.mask)
        x = rand(5, 5)
        @test_throws DimensionMismatch vd(x)
        ULMFiT.reset_masks!(vd)
        @test vd.reset
        Flux.testmode!(vd)
        @test ~vd.active
        x = rand(10, 10)
        @test vd(x) == x
        @test size(vd(x)) == size(x) == size(vd.mask)
    end

    @testset "DroppedEmbeddings" begin
        de = ULMFiT.DroppedEmbeddings(6, 4, 0.2)
        @test size(de.emb) == (6, 4)
        @test size(de.mask) == (6,)
        x = [2,4,6,0.1]
        @test_throws BoundsError de(x)
        x = [2,4,6]
        @test size(de(x)) == (4, 3)
        x = rand(5, 3)
        @test_throws DimensionMismatch de(x, true)
        x = rand(4, 3)
        @test size(de(x, true)) == (6, 3)
        mask = de.mask
        ULMFiT.reset_masks!(de)
        mask != de.mask
        Flux.testmode!(de)
        @test ~de.active
        @test length(params(de)) == 1
    end

    @testset "PooledDense" begin
        pd = ULMFiT.PooledDense(10, 5)
        @test size(pd.W) == (5, 30)
        @test length(pd.b) == 5
        x = rand(Float32, 10, 3)
        @test_throws DimensionMismatch pd(x)
        @test size(pd([x])) == (5, 3)
        @test length(params(pd)) == 2
    end
end

@testset "Language model" begin
    lm = ULMFiT.LanguageModel()
    @test typeof(lm.vocab) == Vector{String}
    @test length(lm.vocab) == size(lm.layers[1].emb, 1)
    @test length(lm.layers) == 10
    @test length(params(lm)) == 16
    @test length(ULMFiT.get_trainable_params(lm.layers)) == 10

    pretrained_weights = BSON.load(datadep"Pretrained ULMFiT Language Model/ulmfit_lm_en.bson")
    @test length(pretrained_weights[:weights]) == 16
    @test all(size.(params(lm)) .== size.(pretrained_weights[:weights]))
end

@testset "Text Classifier" begin
    lm = ULMFiT.LanguageModel()
    tc = ULMFiT.TextClassifier(lm)
    @test tc.rnn_layers == lm.layers[1:8]
    @test length(tc.linear_layers) == 6
end


# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

#=doc
SVMLight / LibSVM is a popular data format for sparse features. Some preprocessed
datasets in this format could be found at http://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/
=#
using MXNet
using SVMLightLoader

mutable struct SVMLightProvider <: mx.AbstractDataProvider
  filename   :: AbstractString
  batch_size :: Int
  fea_dim    :: Int
  data_name  :: Symbol
  label_name :: Symbol
end

function SVMLightProvider(filename::AbstractString, batch_size::Int; fea_dim::Int=-1,
                          data_name::Symbol=:data, label_name::Symbol=:label)
  if fea_dim == -1
    info("SVMLightProvider: going over file to get feature dimension of $filename")
    f = SVMLightFile(filename)
    for (data, label) in f
      fea_dim = max(fea_dim, length(data))
    end
  end

  return SVMLightProvider(filename, batch_size, fea_dim, data_name, label_name)
end

mx.get_batch_size(provider :: SVMLightProvider) = provider.batch_size
function mx.provide_data(provider :: SVMLightProvider)
  [(provider.data_name, (provider.fea_dim, provider.batch_size))]
end
function mx.provide_label(provider :: SVMLightProvider)
  [(provider.label_name, (provider.batch_size,))]
end

function mx.eachbatch(provider :: SVMLightProvider)
  data_jl  = zeros(mx.MX_float, (provider.fea_dim, provider.batch_size))
  data_nd  = mx.empty(size(data_jl))
  label_jl = zeros(mx.MX_float, (provider.batch_size,))
  label_nd = mx.empty(size(label_jl))

  batch = mx.DataBatch([data_nd], [label_nd], provider.batch_size)
  function _svmlight_iter()
    f = SVMLightFile(provider.filename)
    while true
      error("This is actually buggy and needs fixing")
      raw = collect(take(f, provider.batch_size))
      cnt = length(raw)
      if cnt == 0
        # end of file, no more data to see
        return
      end

      data_jl[:] = 0
      for i = 1:provider.batch_size
        vec, gnd = raw[min(i,cnt)]
        data_jl[1:length(vec),i] = vec
        label_jl[i]  = gnd
      end
      mx.copy!(data_nd, data_jl)
      mx.copy!(label_nd, label_jl)
      batch.count = cnt
      produce(batch)
    end
  end

  return Task(_svmlight_iter)
end


mutable struct PlanktonDiagnostics
    plankton::NamedTuple       # for each species
    tracer::NamedTuple         # for tracers
    time_interval::Int64       # time interval that the diagnostics is time averaged
end

"""
    PlanktonDiagnostics(model; tracer=(:PAR, :NH4, :NO3, :DOC),
                        plankton=(:num, :graz, :mort, :dvid),
                        time_interval = 3600)

Generate a `PlanktonDiagnostics` structure.

Keyword Arguments (Optional)
============================
- `tracer` : a `Tuple` containing the names of nutrient fields to be diagnosed.
- `plankton` : a `Tuple` containing the names of physiological processes of plankton individuals to be diagnosed.
- `time_interval` : The time interval that diagnostics is averaged, an hour (3600 seconds) by default.
"""
function PlanktonDiagnostics(model; tracer=(:PAR,),
                            plankton=(:num, :graz, :mort, :dvid),
                            time_interval::Int64 = 3600)

    @assert isa(tracer, Tuple)
    @assert isa(plankton, Tuple)

    diag_avail(tracer, plankton, model.mode)

    ntr   = length(tracer)
    nproc = length(plankton)
    trs   = []
    procs = []

    total_size = (model.grid.Nx+model.grid.Hx*2, model.grid.Ny+model.grid.Hy*2, model.grid.Nz+model.grid.Hz*2)

    for i in 1:ntr
        tr = zeros(total_size) |> array_type(model.arch)
        push!(trs, tr)
    end
    tr_d = zeros(total_size) |> array_type(model.arch)
    tr_default = (PAR = tr_d,)

    diag_tr = NamedTuple{tracer}(trs)
    diag_tr = merge(diag_tr, tr_default) # add PAR as default diagnostic

    plank_name = keys(model.individuals.phytos)
    Nsp = length(plank_name)

    for j in 1:Nsp
        procs_sp = []
        for k in 1:nproc
            proc = zeros(total_size) |> array_type(model.arch)
            push!(procs_sp, proc)
        end
        diag_proc = NamedTuple{plankton}(procs_sp)

        procs_sp_d = []
        for l in 1:4
            proc = zeros(total_size) |> array_type(model.arch)
            push!(procs_sp_d, proc)
        end
        diag_proc_default = NamedTuple{(:num, :graz, :mort, :dvid)}(procs_sp_d)

        diag_proc = merge(diag_proc, diag_proc_default) # add num, graz, mort, and dvid as default diagnostics

        push!(procs, diag_proc)
    end
    diag_sp = NamedTuple{plank_name}(procs)

    diagnostics = PlanktonDiagnostics(diag_sp, diag_tr, time_interval)

    return diagnostics
end

function show(io::IO, diags::PlanktonDiagnostics)
    print(io, "PlanktonDiagnostics:\n",
              "├── diagnostics of tracers: $(keys(diags.tracer))\n",
              "├── diagnostics of individuals: $(keys(diags.plankton.sp1))\n",
              "└── save averaged diagnostics every $(diags.time_interval) seconds")
end

function diag_avail(tracer, plank, mode::AbstractMode)
    tracer_avail = tracer_avail_diags()
    plank_avail  = plank_avail_diags(mode)
    for i in 1:length(tracer)
        if length(findall(x->x==tracer[i], tracer_avail)) == 0
            throw(ArgumentError("$(tracer[i]) is not one of the diagnostics"))
        end
    end

    for i in 1:length(plank)
        if length(findall(x->x==plank[i], plank_avail)) == 0
            throw(ArgumentError("$(plank[i]) is not one of the diagnostics"))
        end
    end
end

function tracer_avail_diags()
    return (:PAR, :DIC, :DOC, :POC, :NH4, :NO3, :DON, :PON, :PO4, :DOP, :POP)
end

function plank_avail_diags(mode::AbstractMode)
    plank_avail = (:num, :graz, :mort, :dvid, :PS, :resp, :Bm, :Chl, :Th)
    if isa(mode, CarbonMode)
        nothing
    elseif isa(mode, QuotaMode)
        plank_avail = (:num, :graz, :mort, :dvid, :PS, :BS, :VDOC, :VNH4, :VNO3, :VPO4, :resp, :exu, :Bm, :Cq, :Nq, :Pq, :Chl)
    elseif isa(mode, MacroMolecularMode)
        plank_avail = (:num, :graz, :mort, :dvid, :PS, :VDOC, :VNH4, :VNO3, :VPO4, :resp, :ρChl, :S_PRO, :S_DNA, :S_RNA, :exu, :CH, :NST, :PST, :PRO, :DNA, :RNA, :Chl)
    end
    return plank_avail
end


module ClpMathProgSolverInterface
using Clp.ClpCInterface
using Compat
using Compat.LinearAlgebra
using Compat.SparseArrays

import MathProgBase
const MPB = MathProgBase

export ClpMathProgModel, ClpSolver

mutable struct ClpMathProgModel <: MPB.AbstractLinearQuadraticModel
    inner::ClpModel
    solveroptions::ClpSolve
end

struct ClpSolver <: MPB.AbstractMathProgSolver
    options
end
ClpSolver(;kwargs...) = ClpSolver(kwargs)

### Options

# map option name to C function
const optionmap = Dict(
   :PrimalTolerance => set_primal_tolerance,
   :DualTolerance => set_dual_tolerance,
   :DualObjectiveLimit => set_dual_objective_limit,
   :MaximumIterations => set_maximum_iterations,
   :MaximumSeconds => set_maximum_seconds,
   :LogLevel => set_log_level,
   :Scaling => scaling,
   :Perturbation => set_perturbation,
   #:Algorithm => set_algorithm
   )
# These options are set by using the ClpSolve object
const solveoptionmap = Dict(
   :PresolveType => set_presolve_type,
   :SolveType => set_solve_type,
   :InfeasibleReturn => set_infeasible_return,
   )

function setoption(m::ClpMathProgModel, name::Symbol, value)
    if haskey(optionmap, name)
        optionmap[name](m.inner,value)
    elseif haskey(solveoptionmap, name)
        solveoptionmap[name](m.solveroptions,value)
    else
        error("Unrecognized option: $name")
    end
end

function ClpMathProgModel(;kwargs...)
    m = ClpMathProgModel(ClpModel(),ClpSolve())
    set_log_level(m.inner,0) # disable output by default
    for (name, value) in kwargs
        setoption(m, name, value)
    end
    return m
end

MPB.LinearQuadraticModel(s::ClpSolver) = ClpMathProgModel(;s.options...)

MPB.ConicModel(s::ClpSolver) = MPB.LPQPtoConicBridge(MPB.LinearQuadraticModel(s))
MPB.supportedcones(s::ClpSolver) = [:Free,:Zero,:NonNeg,:NonPos]

function MPB.loadproblem!(m::ClpMathProgModel, filename::AbstractString)
    if endswith(filename,".mps") || endswith(filename,".mps.gz")
       read_mps(m.inner,filename)
    else
       error("unrecognized input format extension in $filename")
    end
end

function MPB.loadproblem!(m::ClpMathProgModel, A, collb, colub, obj, rowlb,
                          rowub, sense)
    load_problem(m.inner,A,collb,colub,obj,rowlb,rowub)
    MPB.setsense!(m, sense)
end

#writeproblem(m, filename::String)

function replaceInf(x)
    for i in 1:length(x)
        if x[i] > 1e20
            x[i] = Inf
        elseif x[i] < -1e20
            x[i] = -Inf
        end
    end
    return x
end

MPB.getvarLB(m::ClpMathProgModel) = replaceInf(get_col_lower(m.inner))
MPB.setvarLB!(m::ClpMathProgModel, collb) = chg_column_lower(m.inner, collb)

MPB.getvarUB(m::ClpMathProgModel) = replaceInf(get_col_upper(m.inner))
MPB.setvarUB!(m::ClpMathProgModel, colub) = chg_column_upper(m.inner, colub)

MPB.getconstrLB(m::ClpMathProgModel) = replaceInf(get_row_lower(m.inner))
MPB.setconstrLB!(m::ClpMathProgModel, rowlb) = chg_row_lower(m.inner, rowlb)

MPB.getconstrUB(m::ClpMathProgModel) = replaceInf(get_row_upper(m.inner))
MPB.setconstrUB!(m::ClpMathProgModel, rowub) = chg_row_upper(m.inner, rowub)

MPB.getobj(m::ClpMathProgModel) = get_obj_coefficients(m.inner)
MPB.setobj!(m::ClpMathProgModel, obj) = chg_obj_coefficients(m.inner, obj)

function MPB.getconstrmatrix(m::ClpMathProgModel)
    A = get_constraint_matrix(m.inner)
    return convert(SparseMatrixCSC{Float64,Int},A)
end

function MPB.addvar!(m::ClpMathProgModel, rowidx, rowcoef, collb, colub, objcoef)
    @assert length(rowidx) == length(rowcoef)
    colstarts = Int32[0, length(rowcoef)]
    rows = Int32[ i - 1 for i in rowidx ]
    add_columns(m.inner, 1, Float64[collb], Float64[colub], Float64[objcoef],
       colstarts, rows, convert(Vector{Float64},rowcoef))
end

function MPB.addconstr!(m::ClpMathProgModel, colidx, colcoef, rowlb, rowub)
    @assert length(colidx) == length(colcoef)
    rowstarts = Int32[0, length(colcoef)]
    cols = Int32[ i - 1 for i in colidx ]
    add_rows(m.inner, 1, Float64[rowlb], Float64[rowub], rowstarts, cols, convert(Vector{Float64}, colcoef))
end

function MPB.setsense!(m::ClpMathProgModel,sense)
    if sense == :Min
        set_obj_sense(m.inner, 1.0)
    elseif sense == :Max
        set_obj_sense(m.inner, -1.0)
    else
        error("Unrecognized objective sense $sense")
    end
end

function MPB.getsense(m::ClpMathProgModel)
    s = get_obj_sense(m.inner)
    if s == 1.0
        return :Min
    elseif s == -1.0
        return :Max
    else
        error("Internal library error")
    end
end

MPB.numvar(m::ClpMathProgModel) = get_num_cols(m.inner)
MPB.numconstr(m::ClpMathProgModel) = get_num_rows(m.inner)

MPB.optimize!(m::ClpMathProgModel) = initial_solve_with_options(m.inner,m.solveroptions)

function MPB.status(m::ClpMathProgModel)
   s = ClpCInterface.status(m.inner)
   if s == 0
       return :Optimal
   elseif s == 1
       return :Infeasible
   elseif s == 2
       return :Unbounded
   elseif s == 3
       return :UserLimit
   elseif s == 4
       return :Error
   else
       error("Internal library error")
   end
end

MPB.getobjval(m::ClpMathProgModel) = objective_value(m.inner)

MPB.getsolution(m::ClpMathProgModel) = primal_column_solution(m.inner)
MPB.getconstrsolution(m::ClpMathProgModel) = primal_row_solution(m.inner)
MPB.getreducedcosts(m::ClpMathProgModel) = dual_column_solution(m.inner)

MPB.getconstrduals(m::ClpMathProgModel) = dual_row_solution(m.inner)

function MPB.getinfeasibilityray(m::ClpMathProgModel)
    return Compat.rmul!(infeasibility_ray(m.inner),-1.0)
end
MPB.getunboundedray(m::ClpMathProgModel) = unbounded_ray(m.inner)

const statmap = Dict(zip([ 0x00,  0x01,            0x02,            0x03,       0x04,  0x05],
                     [:Free,:Basic,:NonbasicAtUpper,:NonbasicAtLower,:Superbasic,:Fixed]))
function MPB.getbasis(m::ClpMathProgModel)
    num_cols = MPB.numvar(m)
    num_rows = MPB.numconstr(m)
    cbasis = Array{Symbol}(undef, num_cols)
    rbasis = Array{Symbol}(undef, num_rows)
    for i in 1:num_cols
        val = get_column_status(m.inner, i)
        cbasis[i] = statmap[val]
    end
    for i in 1:num_rows
        val = get_row_status(m.inner, i)
        rbasis[i] = statmap[val]
    end
    return cbasis, rbasis
end

MPB.getvartype(m::ClpMathProgModel) = fill(:Cont, get_num_cols(m.inner))
function MPB.setvartype!(m::ClpMathProgModel, typ::Vector{Symbol})
    all(x->isequal(x,:Cont), typ) || error("Clp does not support integer variables")
    return nothing
end

MPB.getrawsolver(m::ClpMathProgModel) = m.inner

end


# This file is a part of BAT.jl, licensed under the MIT License (MIT).

# ToDo: Use
# const Negative{F} = ComposedFunction{typeof(-),F}
# instead of custom struct Negative
# Move ValueShapes.varshape(f::Negative) to ValueShapes

struct Negative{F<:Base.Callable} <: Function
    orig_f::F
end

@inline (f::Negative)(x) = - f.orig_f(x)

ValueShapes.varshape(f::Negative) = varshape(f.orig_f)
ValueShapes.unshaped(f::Negative) = Negative(unshaped(f.orig_f))

function Base.show(io::IO, f::Negative)
    print(io, Base.typename(typeof(f)).name, "(")
    show(io, f.orig_f)
    print(io, ")")
end

Base.show(io::IO, M::MIME"text/plain", f::Negative) = show(io, f)

function negative end
negative(f::Base.Callable) = Negative(f)
negative(f::Negative) = f.orig_f


# Exports
export satellite_plan_baseline
export satellite_plan_resource_baseline

"""
Binary search method to find the first opportunity with an index greater than
the target simulation time (Epoch or elapsed time)
"""
function find_next_opp_index(problem::SatPlanningProblem, target::Union{Epoch, Real})
    # Set search indices
    l = 1
    r = length(problem.opportunities)
    m = 0

    # Perform binary search to get first opportunity with start time
    # greater or equal to the target value
    while l < r
        m = floor(Int, (l+r)/2)
        # println("l: $l, r: $r, avg: $((l+r)/2), m: $m, t: $(problem.opportunities[m].t_start)")

        if problem.opportunities[m].t_start < target
            l = m + 1
        else
            r = m
        end
    end

    return m
end

function baseline_step(problem::SatPlanningProblem, state::SatMDPState, action::Opportunity)
    # If done return same state, just done
    if typeof(action) == Done
        return SatMDPState(time=state.time,
                last_action=Done(t_start=state.time),
                last_cdo_action=state.last_cdo_action,
                request_ids=copy(state.request_ids),
                power=state.power,
                data=state.data)
    end

    # Current state time
    time0 = state.time
    time  = state.time

    # Store last viable action
    last_cdo_action = state.last_cdo_action

    # Resource
    power = state.power
    data  = state.data

    # Copy observed requests
    request_ids = copy(state.request_ids)

    # Finish state
    done = false

    # Advance time to next action
    time = action.t_start

    # Update data generate
    power_generated = 0.0
    data_generated  = problem.spacecraft[1].datagen_backorbit*(time - time0)

    if typeof(action) == Noop
        # Do nothing if Noop
    elseif typeof(action) == Sunpoint
        # Charge for duration of sunpoint
        power_generated += problem.spacecraft[1].powergen_sunpoint*(time - time0)

    elseif typeof(action) == Collect
        # Update last action
        last_cdo_action = action

        collect_generation = action.duration * problem.spacecraft[1].datagen_image

        if data + collect_generation < 1.0
            # Only perform collect if we have capacilty
            data_generated  += collect_generation
            power_generated += action.duration * problem.spacecraft[1].powergen_image

            push!(request_ids, action.location.id)
        end

    elseif typeof(action) == Contact
        # Update last action
        last_cdo_action = action

        data_generated  += action.duration * problem.spacecraft[1].datagen_downlink
        power_generated += action.duration * problem.spacecraft[1].powergen_downlink
    else
        throw(ErrorException("Action $(string(action)) is not of a known type."))
    end

    # Update power and data numbers
    power += power_generated
    data  += data_generated

    # Ensure resource limits are enforced
    if power > 1.0 power = 1.0 end

    if power < 0.0
        power = 0.0
        done = true
    end

    if data > 1.0 data = 1.0 end
    if data < 0.0 data = 0.0 end

    return SatMDPState(time=time,
            last_action=action,
            last_cdo_action=last_cdo_action,
            request_ids=request_ids,
            power=power,
            data=data)
end

function next_feasible_action(problem::SatPlanningProblem, state::SatMDPState)

    # Find index of next possible action
    idx_start = find_next_opp_index(problem::SatPlanningProblem, state.time)

    if idx_start > length(problem.opportunities)
        return nothing
    end

    # Ensure index is for next action after current state
    while problem.opportunities[idx_start].t_start <= state.time
        idx_start += 1

        if idx_start > length(problem.opportunities)
            return nothing
        end
    end

    # Return all opportunities inside planning horizon
    fopps = problem.opportunities[idx_start:end]

    # List of all possible actions
    actions = Opportunity[]

    # Populate list of possible actions
    for opp in fopps
        # Skip Collects until contact if high data
        if typeof(opp) == Collect && state.data >= 0.5
            continue
        end

        # Valuate transition to see if valid
        valid = true

        # Check to see if each transition constraint is valid
        for constraint in problem.constraints
            if valid == false
                break
            end

            valid = valid && constraint(state.last_cdo_action, opp)
        end

        # If valid transition add to edges
        if valid == true
            return opp
        end
    end

    return nothing
end

function satellite_plan_baseline(problem::SatPlanningProblem)

    # Get Initial State
    init_opp = problem.opportunities[1]
    state = SatMDPState(time=init_opp.t_start, last_action=init_opp, last_cdo_action=init_opp)

    states = SatMDPState[state]
    plan = Opportunity[problem.opportunities[1]]
    requests = Request[]

    while next_feasible_action(problem, state) != nothing
        action = next_feasible_action(problem, state)

        # Advance state
        state = baseline_step(problem, state, action)

        push!(states, state)
        push!(plan, action)
    end

    reward = 0.0

    for action in plan
        if typeof(action) == Collect
            reward += action.reward
        end
    end

    return plan, reward
end

function satellite_plan_resource_baseline(problem::SatPlanningProblem; allow_repeats::Bool=false)

    # Get Initial State
    init_opp = problem.opportunities[1]
    state = SatMDPState(time=init_opp.t_start, last_action=init_opp, last_cdo_action=init_opp)
    action = state.last_cdo_action

    states = SatMDPState[state]
    plan = Opportunity[problem.opportunities[1]]
    requests = Request[]

    while next_feasible_action(problem, state) != nothing
        # Get next feasible action
        action = next_feasible_action(problem, state)

        # If able to take collect or downlink do it
        power_generated = 0.0
        data_generated  = problem.spacecraft[1].datagen_backorbit*(action.t_start - state.time)

        if typeof(action) == Collect
            # Resources consumed by action
            data_generated  += action.duration * problem.spacecraft[1].datagen_image
            power_generated += action.duration * problem.spacecraft[1].powergen_image
        elseif typeof(action) == Contact
            # Update data generation
            data_generated  += action.duration * problem.spacecraft[1].datagen_downlink
            power_generated += action.duration * problem.spacecraft[1].powergen_downlink
        end

        if 0.0 < state.power + power_generated && state.data + data_generated < 1.0
            # Take action if able (e.g. do nothing)
        else
            # If not able sunpoint
            action = Sunpoint(t_start=action.t_start)
        end

        # println("State: $state")
        # println("Action: $action")

        # Advance state
        state = baseline_step(problem, state, action)

        push!(states, state)
        push!(plan, action)
    end

    reward = 0.0

    for action in plan
        if typeof(action) == Collect
            reward += action.reward
        end
    end

    return states, plan, reward
end


function abs(obj::FieldDerivativeStructure)
    return jcall(obj, "abs", FieldDerivativeStructure, ())
end

function acos(obj::FieldDerivativeStructure)
    return jcall(obj, "acos", FieldDerivativeStructure, ())
end

function acosh(obj::FieldDerivativeStructure)
    return jcall(obj, "acosh", FieldDerivativeStructure, ())
end

function add(obj::FieldDerivativeStructure, arg0::FieldDerivativeStructure)
    return jcall(obj, "add", FieldDerivativeStructure, (FieldDerivativeStructure,), arg0)
end

function add(obj::FieldDerivativeStructure, arg0::RealFieldElement)
    return jcall(obj, "add", FieldDerivativeStructure, (RealFieldElement,), arg0)
end

function add(obj::FieldDerivativeStructure, arg0::jdouble)
    return jcall(obj, "add", FieldDerivativeStructure, (jdouble,), arg0)
end

function asin(obj::FieldDerivativeStructure)
    return jcall(obj, "asin", FieldDerivativeStructure, ())
end

function asinh(obj::FieldDerivativeStructure)
    return jcall(obj, "asinh", FieldDerivativeStructure, ())
end

function atan(obj::FieldDerivativeStructure)
    return jcall(obj, "atan", FieldDerivativeStructure, ())
end

function atan2(::Type{FieldDerivativeStructure}, arg0::FieldDerivativeStructure, arg1::FieldDerivativeStructure)
    return jcall(FieldDerivativeStructure, "atan2", FieldDerivativeStructure, (FieldDerivativeStructure, FieldDerivativeStructure), arg0, arg1)
end

function atan2(obj::FieldDerivativeStructure, arg0::FieldDerivativeStructure)
    return jcall(obj, "atan2", FieldDerivativeStructure, (FieldDerivativeStructure,), arg0)
end

function atanh(obj::FieldDerivativeStructure)
    return jcall(obj, "atanh", FieldDerivativeStructure, ())
end

function cbrt(obj::FieldDerivativeStructure)
    return jcall(obj, "cbrt", FieldDerivativeStructure, ())
end

function ceil(obj::FieldDerivativeStructure)
    return jcall(obj, "ceil", FieldDerivativeStructure, ())
end

function compose(obj::FieldDerivativeStructure, arg0::Vector{RealFieldElement})
    return jcall(obj, "compose", FieldDerivativeStructure, (Vector{RealFieldElement},), arg0)
end

function compose(obj::FieldDerivativeStructure, arg0::Vector{jdouble})
    return jcall(obj, "compose", FieldDerivativeStructure, (Vector{jdouble},), arg0)
end

function copy_sign(obj::FieldDerivativeStructure, arg0::FieldDerivativeStructure)
    return jcall(obj, "copySign", FieldDerivativeStructure, (FieldDerivativeStructure,), arg0)
end

function copy_sign(obj::FieldDerivativeStructure, arg0::RealFieldElement)
    return jcall(obj, "copySign", FieldDerivativeStructure, (RealFieldElement,), arg0)
end

function copy_sign(obj::FieldDerivativeStructure, arg0::jdouble)
    return jcall(obj, "copySign", FieldDerivativeStructure, (jdouble,), arg0)
end

function cos(obj::FieldDerivativeStructure)
    return jcall(obj, "cos", FieldDerivativeStructure, ())
end

function cosh(obj::FieldDerivativeStructure)
    return jcall(obj, "cosh", FieldDerivativeStructure, ())
end

function divide(obj::FieldDerivativeStructure, arg0::FieldDerivativeStructure)
    return jcall(obj, "divide", FieldDerivativeStructure, (FieldDerivativeStructure,), arg0)
end

function divide(obj::FieldDerivativeStructure, arg0::RealFieldElement)
    return jcall(obj, "divide", FieldDerivativeStructure, (RealFieldElement,), arg0)
end

function divide(obj::FieldDerivativeStructure, arg0::jdouble)
    return jcall(obj, "divide", FieldDerivativeStructure, (jdouble,), arg0)
end

function exp(obj::FieldDerivativeStructure)
    return jcall(obj, "exp", FieldDerivativeStructure, ())
end

function expm1(obj::FieldDerivativeStructure)
    return jcall(obj, "expm1", FieldDerivativeStructure, ())
end

function floor(obj::FieldDerivativeStructure)
    return jcall(obj, "floor", FieldDerivativeStructure, ())
end

function get_all_derivatives(obj::FieldDerivativeStructure)
    return jcall(obj, "getAllDerivatives", Vector{RealFieldElement}, ())
end

function get_exponent(obj::FieldDerivativeStructure)
    return jcall(obj, "getExponent", jint, ())
end

function get_factory(obj::FieldDerivativeStructure)
    return jcall(obj, "getFactory", FDSFactory, ())
end

function get_field(obj::FieldDerivativeStructure)
    return jcall(obj, "getField", Field, ())
end

function get_free_parameters(obj::FieldDerivativeStructure)
    return jcall(obj, "getFreeParameters", jint, ())
end

function get_order(obj::FieldDerivativeStructure)
    return jcall(obj, "getOrder", jint, ())
end

function get_partial_derivative(obj::FieldDerivativeStructure, arg0::Vector{jint})
    return jcall(obj, "getPartialDerivative", RealFieldElement, (Vector{jint},), arg0)
end

function get_real(obj::FieldDerivativeStructure)
    return jcall(obj, "getReal", jdouble, ())
end

function get_value(obj::FieldDerivativeStructure)
    return jcall(obj, "getValue", RealFieldElement, ())
end

function hypot(::Type{FieldDerivativeStructure}, arg0::FieldDerivativeStructure, arg1::FieldDerivativeStructure)
    return jcall(FieldDerivativeStructure, "hypot", FieldDerivativeStructure, (FieldDerivativeStructure, FieldDerivativeStructure), arg0, arg1)
end

function hypot(obj::FieldDerivativeStructure, arg0::FieldDerivativeStructure)
    return jcall(obj, "hypot", FieldDerivativeStructure, (FieldDerivativeStructure,), arg0)
end

function linear_combination(obj::FieldDerivativeStructure, arg0::FieldDerivativeStructure, arg1::FieldDerivativeStructure, arg2::FieldDerivativeStructure, arg3::FieldDerivativeStructure)
    return jcall(obj, "linearCombination", FieldDerivativeStructure, (FieldDerivativeStructure, FieldDerivativeStructure, FieldDerivativeStructure, FieldDerivativeStructure), arg0, arg1, arg2, arg3)
end

function linear_combination(obj::FieldDerivativeStructure, arg0::FieldDerivativeStructure, arg1::FieldDerivativeStructure, arg2::FieldDerivativeStructure, arg3::FieldDerivativeStructure, arg4::FieldDerivativeStructure, arg5::FieldDerivativeStructure)
    return jcall(obj, "linearCombination", FieldDerivativeStructure, (FieldDerivativeStructure, FieldDerivativeStructure, FieldDerivativeStructure, FieldDerivativeStructure, FieldDerivativeStructure, FieldDerivativeStructure), arg0, arg1, arg2, arg3, arg4, arg5)
end

function linear_combination(obj::FieldDerivativeStructure, arg0::FieldDerivativeStructure, arg1::FieldDerivativeStructure, arg2::FieldDerivativeStructure, arg3::FieldDerivativeStructure, arg4::FieldDerivativeStructure, arg5::FieldDerivativeStructure, arg6::FieldDerivativeStructure, arg7::FieldDerivativeStructure)
    return jcall(obj, "linearCombination", FieldDerivativeStructure, (FieldDerivativeStructure, FieldDerivativeStructure, FieldDerivativeStructure, FieldDerivativeStructure, FieldDerivativeStructure, FieldDerivativeStructure, FieldDerivativeStructure, FieldDerivativeStructure), arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7)
end

function linear_combination(obj::FieldDerivativeStructure, arg0::RealFieldElement, arg1::FieldDerivativeStructure, arg2::RealFieldElement, arg3::FieldDerivativeStructure)
    return jcall(obj, "linearCombination", FieldDerivativeStructure, (RealFieldElement, FieldDerivativeStructure, RealFieldElement, FieldDerivativeStructure), arg0, arg1, arg2, arg3)
end

function linear_combination(obj::FieldDerivativeStructure, arg0::RealFieldElement, arg1::FieldDerivativeStructure, arg2::RealFieldElement, arg3::FieldDerivativeStructure, arg4::RealFieldElement, arg5::FieldDerivativeStructure)
    return jcall(obj, "linearCombination", FieldDerivativeStructure, (RealFieldElement, FieldDerivativeStructure, RealFieldElement, FieldDerivativeStructure, RealFieldElement, FieldDerivativeStructure), arg0, arg1, arg2, arg3, arg4, arg5)
end

function linear_combination(obj::FieldDerivativeStructure, arg0::RealFieldElement, arg1::FieldDerivativeStructure, arg2::RealFieldElement, arg3::FieldDerivativeStructure, arg4::RealFieldElement, arg5::FieldDerivativeStructure, arg6::RealFieldElement, arg7::FieldDerivativeStructure)
    return jcall(obj, "linearCombination", FieldDerivativeStructure, (RealFieldElement, FieldDerivativeStructure, RealFieldElement, FieldDerivativeStructure, RealFieldElement, FieldDerivativeStructure, RealFieldElement, FieldDerivativeStructure), arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7)
end

function linear_combination(obj::FieldDerivativeStructure, arg0::Vector{FieldDerivativeStructure}, arg1::Vector{FieldDerivativeStructure})
    return jcall(obj, "linearCombination", FieldDerivativeStructure, (Vector{FieldDerivativeStructure}, Vector{FieldDerivativeStructure}), arg0, arg1)
end

function linear_combination(obj::FieldDerivativeStructure, arg0::Vector{RealFieldElement}, arg1::Vector{FieldDerivativeStructure})
    return jcall(obj, "linearCombination", FieldDerivativeStructure, (Vector{RealFieldElement}, Vector{FieldDerivativeStructure}), arg0, arg1)
end

function linear_combination(obj::FieldDerivativeStructure, arg0::Vector{jdouble}, arg1::Vector{FieldDerivativeStructure})
    return jcall(obj, "linearCombination", FieldDerivativeStructure, (Vector{jdouble}, Vector{FieldDerivativeStructure}), arg0, arg1)
end

function linear_combination(obj::FieldDerivativeStructure, arg0::jdouble, arg1::FieldDerivativeStructure, arg2::jdouble, arg3::FieldDerivativeStructure)
    return jcall(obj, "linearCombination", FieldDerivativeStructure, (jdouble, FieldDerivativeStructure, jdouble, FieldDerivativeStructure), arg0, arg1, arg2, arg3)
end

function linear_combination(obj::FieldDerivativeStructure, arg0::jdouble, arg1::FieldDerivativeStructure, arg2::jdouble, arg3::FieldDerivativeStructure, arg4::jdouble, arg5::FieldDerivativeStructure)
    return jcall(obj, "linearCombination", FieldDerivativeStructure, (jdouble, FieldDerivativeStructure, jdouble, FieldDerivativeStructure, jdouble, FieldDerivativeStructure), arg0, arg1, arg2, arg3, arg4, arg5)
end

function linear_combination(obj::FieldDerivativeStructure, arg0::jdouble, arg1::FieldDerivativeStructure, arg2::jdouble, arg3::FieldDerivativeStructure, arg4::jdouble, arg5::FieldDerivativeStructure, arg6::jdouble, arg7::FieldDerivativeStructure)
    return jcall(obj, "linearCombination", FieldDerivativeStructure, (jdouble, FieldDerivativeStructure, jdouble, FieldDerivativeStructure, jdouble, FieldDerivativeStructure, jdouble, FieldDerivativeStructure), arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7)
end

function log(obj::FieldDerivativeStructure)
    return jcall(obj, "log", FieldDerivativeStructure, ())
end

function log10(obj::FieldDerivativeStructure)
    return jcall(obj, "log10", FieldDerivativeStructure, ())
end

function log1p(obj::FieldDerivativeStructure)
    return jcall(obj, "log1p", FieldDerivativeStructure, ())
end

function multiply(obj::FieldDerivativeStructure, arg0::FieldDerivativeStructure)
    return jcall(obj, "multiply", FieldDerivativeStructure, (FieldDerivativeStructure,), arg0)
end

function multiply(obj::FieldDerivativeStructure, arg0::RealFieldElement)
    return jcall(obj, "multiply", FieldDerivativeStructure, (RealFieldElement,), arg0)
end

function multiply(obj::FieldDerivativeStructure, arg0::jdouble)
    return jcall(obj, "multiply", FieldDerivativeStructure, (jdouble,), arg0)
end

function multiply(obj::FieldDerivativeStructure, arg0::jint)
    return jcall(obj, "multiply", FieldDerivativeStructure, (jint,), arg0)
end

function negate(obj::FieldDerivativeStructure)
    return jcall(obj, "negate", FieldDerivativeStructure, ())
end

function new_instance(obj::FieldDerivativeStructure, arg0::jdouble)
    return jcall(obj, "newInstance", FieldDerivativeStructure, (jdouble,), arg0)
end

function pow(::Type{FieldDerivativeStructure}, arg0::jdouble, arg1::FieldDerivativeStructure)
    return jcall(FieldDerivativeStructure, "pow", FieldDerivativeStructure, (jdouble, FieldDerivativeStructure), arg0, arg1)
end

function pow(obj::FieldDerivativeStructure, arg0::FieldDerivativeStructure)
    return jcall(obj, "pow", FieldDerivativeStructure, (FieldDerivativeStructure,), arg0)
end

function pow(obj::FieldDerivativeStructure, arg0::jdouble)
    return jcall(obj, "pow", FieldDerivativeStructure, (jdouble,), arg0)
end

function pow(obj::FieldDerivativeStructure, arg0::jint)
    return jcall(obj, "pow", FieldDerivativeStructure, (jint,), arg0)
end

function reciprocal(obj::FieldDerivativeStructure)
    return jcall(obj, "reciprocal", FieldDerivativeStructure, ())
end

function remainder(obj::FieldDerivativeStructure, arg0::FieldDerivativeStructure)
    return jcall(obj, "remainder", FieldDerivativeStructure, (FieldDerivativeStructure,), arg0)
end

function remainder(obj::FieldDerivativeStructure, arg0::RealFieldElement)
    return jcall(obj, "remainder", FieldDerivativeStructure, (RealFieldElement,), arg0)
end

function remainder(obj::FieldDerivativeStructure, arg0::jdouble)
    return jcall(obj, "remainder", FieldDerivativeStructure, (jdouble,), arg0)
end

function rint(obj::FieldDerivativeStructure)
    return jcall(obj, "rint", FieldDerivativeStructure, ())
end

function root_n(obj::FieldDerivativeStructure, arg0::jint)
    return jcall(obj, "rootN", FieldDerivativeStructure, (jint,), arg0)
end

function scalb(obj::FieldDerivativeStructure, arg0::jint)
    return jcall(obj, "scalb", FieldDerivativeStructure, (jint,), arg0)
end

function signum(obj::FieldDerivativeStructure)
    return jcall(obj, "signum", FieldDerivativeStructure, ())
end

function sin(obj::FieldDerivativeStructure)
    return jcall(obj, "sin", FieldDerivativeStructure, ())
end

function sin_cos(obj::FieldDerivativeStructure)
    return jcall(obj, "sinCos", FieldSinCos, ())
end

function sinh(obj::FieldDerivativeStructure)
    return jcall(obj, "sinh", FieldDerivativeStructure, ())
end

function sqrt(obj::FieldDerivativeStructure)
    return jcall(obj, "sqrt", FieldDerivativeStructure, ())
end

function subtract(obj::FieldDerivativeStructure, arg0::FieldDerivativeStructure)
    return jcall(obj, "subtract", FieldDerivativeStructure, (FieldDerivativeStructure,), arg0)
end

function subtract(obj::FieldDerivativeStructure, arg0::RealFieldElement)
    return jcall(obj, "subtract", FieldDerivativeStructure, (RealFieldElement,), arg0)
end

function subtract(obj::FieldDerivativeStructure, arg0::jdouble)
    return jcall(obj, "subtract", FieldDerivativeStructure, (jdouble,), arg0)
end

function tan(obj::FieldDerivativeStructure)
    return jcall(obj, "tan", FieldDerivativeStructure, ())
end

function tanh(obj::FieldDerivativeStructure)
    return jcall(obj, "tanh", FieldDerivativeStructure, ())
end

function taylor(obj::FieldDerivativeStructure, arg0::Vector{RealFieldElement})
    return jcall(obj, "taylor", RealFieldElement, (Vector{RealFieldElement},), arg0)
end

function taylor(obj::FieldDerivativeStructure, arg0::Vector{jdouble})
    return jcall(obj, "taylor", RealFieldElement, (Vector{jdouble},), arg0)
end

function to_degrees(obj::FieldDerivativeStructure)
    return jcall(obj, "toDegrees", FieldDerivativeStructure, ())
end

function to_radians(obj::FieldDerivativeStructure)
    return jcall(obj, "toRadians", FieldDerivativeStructure, ())
end


# =============================
# Vector Field for continuous system
# =============================

"""
    vector_field(system::ContinuousIdentitySystem, x::AbstractVector)

Return the vector field state of a `ContinuousIdentitySystem`.

### Input

- `system` -- `ContinuousIdentitySystem`
- `x`      -- state (it should be any vector type)

### Output

A zeros vector of dimension `statedim`.
"""
function vector_field(system::ContinuousIdentitySystem, x::AbstractVector)
    !_is_conformable_state(system, x) && _argument_error(:x)
    return zeros(statedim(system))
end

"""
    vector_field(system::ConstrainedContinuousIdentitySystem, x::AbstractVector;
              [check_constraints]=true)

Return the vector field state of a `ConstrainedContinuousIdentitySystem`.

### Input

- `system`            -- `ConstrainedContinuousIdentitySystem`
- `x`                 -- state (it should be any vector type)
- `check_constraints` -- (optional, default: `true`) check if the state belongs to
                         the state set

### Output

A zeros vector of dimension `statedim`.
"""
function vector_field(system::ConstrainedContinuousIdentitySystem, x::AbstractVector;
                   check_constraints::Bool=true)
    !_is_conformable_state(system, x) && _argument_error(:x)
    if check_constraints
        !_in_stateset(system, x) && _argument_error(:x,:X)
    end
    return zeros(statedim(system))
end

"""
    vector_field(system::AbstractContinuousSystem, x::AbstractVector;
              [check_constraints]=true)

Return the vector field state of an `AbstractContinuousSystem`.

### Input

- `system`            -- `AbstractContinuousSystem`
- `x`                 -- state (it should be any vector type)
- `check_constraints` -- (optional, default: `true`) check if the state belongs to
                         the state set

### Output

The vector field of the system at state `x`.
"""
vector_field(system::AbstractContinuousSystem, x::AbstractVector; kwargs...) =
    _instantiate(system, x; kwargs...)

"""
    vector_field(system::AbstractContinuousSystem, x::AbstractVector, u::AbstractVector;
              [check_constraints]=true)

Return the vector field state of an `AbstractContinuousSystem`.

### Input

- `system`            -- `AbstractContinuousSystem`
- `x`                 -- state (it should be any vector type)
- `u`                 -- input (it should be any vector type) or noise, if `system`
                         is not controlled
- `check_constraints` -- (optional, default: `true`) check if the state belongs to
                         the state set

### Output

The vector field of the system at state `x` and applying input `u`.

### Notes

If the system is not controlled but noisy, the input `u` is interpreted as noise.
    """
vector_field(system::AbstractContinuousSystem, x::AbstractVector, u::AbstractVector; kwargs...) =
    _instantiate(system, x, u; kwargs...)

"""
    vector_field(system::AbstractContinuousSystem,
              x::AbstractVector, u::AbstractVector, w::AbstractVector; [check_constraints]=true)

Return the vector field state of an `AbstractContinuousSystem`.

### Input

- `system`            -- `AbstractContinuousSystem`
- `x`                 -- state (it should be any vector type)
- `u`                 -- input (it should be any vector type)
- `w`                 -- noise (it should be any vector type)
- `check_constraints` -- (optional, default: `true`) check if the state belongs to
                         the state set

### Output

The vector field of the system at state `x` and applying input `u` and noise `w`.
"""
vector_field(system::AbstractContinuousSystem, x::AbstractVector, u::AbstractVector, w::AbstractVector; kwargs...) =
    _instantiate(system, x, u, w; kwargs...)

# =============================
# VectorField type for continuous system
# =============================
"""
    VectorField{T<:Function}

Type that computes the vector field of an `AbstractContinuousSystem`.

### Fields

- `field`  -- function for calculating the vector field
"""
struct VectorField{T<:Function}
    field::T
end

# function-like evaluation
@inline function (V::VectorField)(args...)
    evaluate(V, args...)
end

function evaluate(V::VectorField, args...)
    return V.field(args...)
end

"""
    VectorField(sys::AbstractContinuousSystem)

Constructor for creating a `VectorField` from an `AbstractContinuousSystem`.

### Input

- `sys` -- `AbstractContinuousSystem`

### Ouptut

The `VectorField` for the continuous system `sys`.
"""
function VectorField(sys::AbstractContinuousSystem)
    if inputdim(sys) == 0 && noisedim(sys) == 0
        field = (x) -> vector_field(sys, x)
    elseif inputdim(sys) == 0 || noisedim(sys) == 0
        field = (x, u) -> vector_field(sys, x, u)
    else
        field = (x, u, w) -> vector_field(sys, x, u, w)
    end
    return VectorField(field)
end

function _position_value_list(V::VectorField, grid, dimx, dimy)
    X, Y = grid
    x1 = X[1]
    y1 = Y[1]
    N_POS = typeof(x1)
    N_VAL = typeof(V([x1, y1])[dimx])
    m = length(X) * length(Y)

    x = Vector{N_POS}(undef, m)  # x coordinates of each
    y = similar(x)
    vx = Vector{N_VAL}(undef, m)
    vy = similar(vx)
    k = 1
    max_entry = N_VAL(-Inf)
    for xi in X
        for yj in Y
            x[k] = xi
            y[k] = yj
            val = V([xi, yj])
            vx_k = val[dimx]
            vy_k = val[dimy]
            max_entry = max(max_entry, abs(vx_k), abs(vy_k))
            vx[k] = vx_k
            vy[k] = vy_k
            k += 1
        end
    end

    # normalize
    if max_entry > zero(N_VAL)
        step_x = (maximum(X) - minimum(X)) / length(X)
        step_y = (maximum(Y) - minimum(Y)) / length(Y)
        max_entry_x = max_entry / step_x
        max_entry_y = max_entry / step_y
        max_entry = max(max_entry_x, max_entry_y)
        for k in 1:m
            vx[k] /= max_entry
            vy[k] /= max_entry
        end
    end

    # filter out (0, 0) entries
    for k in m:-1:1
        if vx[k] == vy[k] == zero(N_VAL)
            deleteat!(x, k)
            deleteat!(y, k)
            deleteat!(vx, k)
            deleteat!(vy, k)
        end
    end

    # center arrows in the grid points
    for k in 1:length(x)
        x[k] -= vx[k] / 2
        y[k] -= vy[k] / 2
    end

    return x, y, vx, vy
end

### plot recipe
# arguments:
# - grid_points: pair containing the x and y coordinates of the grid points
#   example: the pair `([-1, 0, 1], [2, 3])` represents the grid of points
#   `[-1, 2], [0, 2], [1, 2], [-1, 3], [0, 3], [1, 3]`
# - dims: the two dimensions to plot
@recipe function plot(V::VectorField;
                      grid_points=[range(-3, stop=3, length=21),
                                   range(-3, stop=3, length=21)],
                      dims=[1, 2])
    seriestype := :quiver

    x, y, vx, vy = _position_value_list(V, grid_points, dims[1], dims[2])
    quiver := (vx, vy)

    (x, y)
end


abstract Time_Operator
export Time_Operator
include("Smoother.jl")
include("Selector.jl")

function TProcess{N}(sst :: Array{Float64,N}, TS :: Time_Operator,
                      Dim :: Tuple{Int64,Vararg{Int64}})
  error("Still No function for pure Time_Operator");
end

function TProcess{N}(sst :: Array{Float64,N}, TS :: Time_Operator, time :: Array{Int64,1},
                      Dim :: Tuple{Int64,Vararg{Int64}} = TS.DefaultDim )
  error("Still No function for pure Time_Operator");
end

include("Simple_Functions.jl")

export Time_Operator


#####
##### Exploring a player's policy
#####

using DataStructures: Stack

# All state statistics and are expressed with respect to the current player

@kwdef mutable struct StateActionStats
  P     :: Union{Float64, Nothing} = nothing  # Probability given by `think`
  Pmem  :: Union{Float64, Nothing} = nothing  # Recorded π component in memory
  Pmcts :: Union{Float64, Nothing} = nothing  # Percentage of MCTS visits
  Qmcts :: Union{Float64, Nothing} = nothing
  UCT   :: Union{Float64, Nothing} = nothing
  Pnet  :: Union{Float64, Nothing} = nothing
  Qnet  :: Union{Float64, Nothing} = nothing
end

mutable struct StateStats{Action}
  Nmcts :: Union{Int, Nothing}
  Nmem  :: Union{Int, Nothing}
  Vmem  :: Union{Float64, Nothing}
  Vnet  :: Union{Float64, Nothing}
  actions :: Vector{Tuple{Action, StateActionStats}} # Sorted by P
  function StateStats(actions)
    actions_stats = [(a, StateActionStats()) for a in actions]
    new{eltype(actions)}(nothing, nothing, nothing, nothing, actions_stats)
  end
end

player_reward(game, white_reward) =
  GI.white_playing(game) ? white_reward : - white_reward

function evaluate_vnet(oracle, game)
  if GI.game_terminated(game)
    return 0.0
  else
    return oracle(GI.current_state(game))[2]
  end
end

function evaluate_qnet(oracle, game, action, gamma)
  @assert !GI.game_terminated(game)
  next = GI.clone(game)
  GI.play!(next, action)
  wr = GI.white_reward(next)
  r = GI.white_playing(game) ? wr : -wr
  qnext = evaluate_vnet(oracle, next)
  if GI.white_playing(game) != GI.white_playing(next)
    qnext = -qnext
  end
  return r + gamma * qnext
end

player_oracle(p) = nothing
player_oracle(p::MctsPlayer) = p.mcts.oracle
player_oracle(p::NetworkPlayer) = p.network

player_gamma(p) = p.gamma
player_gamma(p::MctsPlayer) = p.mcts.gamma

function state_statistics(game, player, turn, memory=nothing)
  @assert !GI.game_terminated(game)
  state = GI.current_state(game)
  # Make the player think
  actions, π = think(player, game)
  τ = player_temperature(player, game, turn)
  π = apply_temperature(π, τ)
  report = StateStats(actions)
  for i in eachindex(actions)
    report.actions[i][2].P = π[i]
  end
  # Collect MCTS Statistics
  if isa(player, MctsPlayer) && haskey(player.mcts.tree, state)
    mcts = player.mcts
    info = mcts.tree[state]
    ucts = MCTS.uct_scores(info, mcts.cpuct, 0., nothing)
    report.Nmcts = MCTS.Ntot(info)
    for (i, a) in enumerate(actions)
      astats = info.stats[i]
      arep = report.actions[i][2]
      arep.Pmcts = astats.N / max(1, report.Nmcts)
      arep.Qmcts = astats.N > 0 ? astats.W / astats.N : 0.
      arep.UCT = ucts[i]
    end
  end
  # Collect memory statistics
  if !isnothing(memory)
    mem = AlphaZero.merge_by_state(get_experience(memory))
    relevant = findall((ex -> ex.s == state), mem)
    if !isempty(relevant)
      @assert length(relevant) == 1
      e = mem[relevant[1]]
      report.Nmem = e.n
      report.Vmem = e.z
      for i in eachindex(actions)
        report.actions[i][2].Pmem = e.π[i]
      end
    end
  end
  # Collect network statistics
  oracle = player_oracle(player)
  if isa(oracle, AbstractNetwork)
    Pnet, Vnet = oracle(state)
    report.Vnet = Vnet
    for i in eachindex(actions)
      arep = report.actions[i][2]
      arep.Pnet = Pnet[i]
      arep.Qnet = evaluate_qnet(oracle, game, actions[i], player_gamma(player))
    end
  end
  # Sort the actions from best to worst
  sortby = ((a, arep),) -> arep.P
  sort!(report.actions, rev=true, by=sortby)
  return report
end

#####
##### Displaying state statistics
#####

function print_state_statistics(gspec, stats::StateStats)
  prob   = Log.ColType(nothing, x -> fmt(".1f", 100 * x) * "%")
  val    = Log.ColType(nothing, x -> fmt("+.2f", x))
  bigint = Log.ColType(nothing, n -> format(ceil(Int, n), commas=true))
  alabel = Log.ColType(nothing, identity)
  btable = Log.Table([
    ("Nmcts", bigint, r -> r.Nmcts),
    ("Nmem",  bigint, r -> r.Nmem),
    ("Vmem",  val,    r -> r.Vmem),
    ("Vnet",  val,    r -> r.Vnet)],
    header_style=Log.BOLD)
  atable = Log.Table([
    ("",      alabel, r -> GI.action_string(gspec, r[1])),
    ("",      prob,   r -> r[2].P),
    ("Pmcts", prob,   r -> r[2].Pmcts),
    ("Pnet",  prob,   r -> r[2].Pnet),
    ("UCT",   val,    r -> r[2].UCT),
    ("Pmem",  prob,   r -> r[2].Pmem),
    ("Qmcts", val,    r -> r[2].Qmcts),
    ("Qnet",  val,    r -> r[2].Qnet)],
    header_style=Log.BOLD)
  logger = Logger()
  if !all(isnothing, [stats.Nmcts, stats.Nmem, stats.Vmem, stats.Vnet])
    Log.table(logger, btable, [stats])
    Log.sep(logger)
  end
  Log.table(logger, atable, stats.actions)
  Log.sep(logger)
end

# Return the stats
function compute_and_print_state_statistics(exp)
  if !GI.game_terminated(exp.game)
    stats = state_statistics(exp.game, exp.player, exp.turn, exp.memory)
    print_state_statistics(GI.spec(exp.game), stats)
    return stats
  else
    return nothing
  end
end

#####
##### Interactive exploration of the environment
#####

mutable struct Explorer
  game :: AbstractGameEnv
  history :: Stack{AbstractGameEnv}
  player :: AbstractPlayer
  memory :: Union{MemoryBuffer, Nothing}
  turn :: Int
  function Explorer(
      player::AbstractPlayer,
      game::AbstractGameEnv;
      memory=nothing)
    history = Stack{AbstractGameEnv}()
    new(game, history, player, memory, 0)
  end
end

function restart!(exp::Explorer)
  reset_player!(exp.player)
  empty!(exp.history)
  exp.game = GI.init(GI.spec(exp.game))
  exp.turn = 0
end

save_game!(exp::Explorer) = push!(exp.history, GI.clone(exp.game))

# Return true if the command is valid, false otherwise
function interpret!(exp::Explorer, stats, cmd, args=[])
  gspec = GI.spec(exp.game)
  if cmd == "go"
    st = GI.read_state(gspec)
    if isnothing(st)
      println("Invalid state description.")
      return false
    end
    g = GI.init(gspec, st)
    if !isnothing(st)
      save_game!(exp)
      exp.game = g
      exp.turn = 0
      return true
    end
  elseif cmd == "restart"
    restart!(exp)
    return true
  elseif cmd == "undo"
    if !isempty(exp.history)
      exp.game = pop!(exp.history)
      exp.turn -= 1
      return true
    end
  elseif cmd == "do"
    GI.game_terminated(exp.game) && return false
    if length(args) == 0
      a = stats.actions[1][1]
    else
      length(args) == 1 || return false
      a = GI.parse_action(GI.spec(exp.game), args[1])
      isnothing(a) && return false
      a ∈ GI.available_actions(exp.game) || return false
    end
    save_game!(exp)
    GI.play!(exp.game, a)
    exp.turn += 1
    return true
  elseif cmd == "flip"
    save_game!(exp)
    GI.apply_random_symmetry!(exp.game)
    return true
  elseif cmd == "explore"
    isa(exp.player, MctsPlayer) || (return false)
    try
      if isempty(args)
        n = exp.player.niters
      else
        n = parse(Int, args[1])
      end
      MCTS.explore!(exp.player.mcts, exp.game, n)
      return true
    catch e
      isa(e, ArgumentError) && (return false)
      rethrow(e)
    end
  elseif cmd == "dump"
    GI.dump_state(Game)
    return true
  end
  return false
end

function start_explorer(exp::Explorer)
  while true
    # Print the state
    GI.render(exp.game)
    stats = compute_and_print_state_statistics(exp)
    # Interpret command
    while true
      print("> ")
      inp = readline() |> lowercase |> split
      isempty(inp) && return
      cmd = inp[1]
      args = inp[2:end]
      interpret!(exp, stats, cmd, args) && break
    end
    println("")
  end
end

#####
##### The explore methods
#####

"""
    explore(::AbstractPlayer, ::AbstractGameEnv;  [memory])
    explore(::AbstractPlayer, ::AbstractGameSpec; [memory])

Start a command interpreter to explore the internals of a player
through interactive play.

The `memory` argument is an optional reference to a memory buffer.
When provided, additional state statistics are displayed.

# Commands

The following commands are currently implemented:

  - `do [action]`: make the current player perform `action`.
    By default, the action of highest score is played.
  - `explore [num_sims]`: run `num_sims` MCTS simulations from the current
    state (for MCTS players only).
  - `go`: query the user for a state description and go to this state.
  - `flip`: flip the board according to a random symmetry.
  - `undo`: undo the effect of the previous command.
  - `restart`: restart the explorer.
"""
function explore end

function explore(
    player::AbstractPlayer,
    game::AbstractGameEnv;
    memory=nothing)
  return start_explorer(Explorer(player, game, memory=memory))
end

function explore(player::AbstractPlayer, gspec::AbstractGameSpec; args...)
  return explore(player, GI.init(gspec), args...)
end


using Test

include("../src/main_model.jl")
let m = getpage("NDCs", false, false)
    run(m)

    while m[:Discontinuity,:occurdis_occurrencedummy] != [0.,0.,0.,0.,0.,0.,0.,1.,1.,1.]
        run(m)
    end

    # Climate component
    temp = m[:ClimateTemperature, :rt_g_globaltemperature]
    temp_compare = readpagedata(m, "test/validationdata/noperm/rt_g_globaltemperature.csv")
    @test temp ≈ temp_compare rtol = 1e-4

    # Abatement Costs
    abatement = m[:TotalAbatementCosts, :tct_totalcosts]
    abatement_compare = readpagedata(m, "test/validationdata/noperm/tct_totalcosts.csv")
    @test abatement ≈ abatement_compare rtol = 1e-2

    te = m[:EquityWeighting, :te_totaleffect]
    te_compare = 9.462179758012009e8
    @test te ≈ te_compare rtol = 1e4
end


using Test
using Checkpointing
using LinearAlgebra
# All tested AD tools
using ForwardDiff, ReverseDiff, Zygote, Enzyme
# Include all the AD tool interfaces through `jacobian()`
include("../examples/adtools.jl")

@testset "Testing Checkpointing.jl" begin
    @testset "Testing Enzyme..." begin
        include("speelpenning.jl")
        errf, errg = main()
        @test isapprox(errf, 0.0; atol = 1e-15)
        @test isapprox(errg, 0.0; atol = 1e-15)
    end
    @testset "Testing Revolve..." begin
        global steps = 50
        global checkpoints = 7
        global verbose = 0
        include("../examples/printaction.jl")
        revolve = main(steps, checkpoints)

        @test revolve.numfwd == 105
        @test revolve.numstore == 28
        @test revolve.numinv == 177
    end

    @testset "Testing Jacobian interface..." begin
        include("jacobian.jl")
    end
    @testset "Test optcontrol" begin
        include("../examples/deprecated/optcontrol.jl")
        @testset "AD Tool $adtool" for adtool in [EnzymeADTool(), ForwardDiffADTool(), ReverseDiffADTool(), ZygoteADTool()]
            @testset "Testing Revolve..." begin
                # Enzyme segfaults if the garbage collector is enabled in this immutable code
                if isa(adtool, EnzymeADTool)
                    GC.gc()
                    GC.enable(false)
                end
                global steps = 100
                global snaps = 3
                global info = 0

                function store(F_H, F_C,t, i)
                    F_C[1,i] = F_H[1]
                    F_C[2,i] = F_H[2]
                    F_C[3,i] = t
                    return
                end

                function restore(F_C, i)
                    F_H = [F_C[1,i], F_C[2,i]]
                    t = F_C[3,i]
                    return F_H, t
                end
                revolve = Revolve(steps, snaps, store, restore; verbose=info)
                F_opt, F_final, L_opt, L = optcontrol(revolve, steps, adtool)
                @test isapprox(F_opt, F_final, rtol=1e-4)
                @test isapprox(L_opt, L, rtol=1e-4)
            end

            @testset "Testing Periodic..." begin
                global steps = 100
                global snaps = 4
                global info = 1

                function store(F_H, F_C,t, i)
                    F_C[1,i] = F_H[1]
                    F_C[2,i] = F_H[2]
                    F_C[3,i] = t
                    return
                end

                function restore(F_C, i)
                    F_H = [F_C[1,i], F_C[2,i]]
                    t = F_C[3,i]
                    return F_H, t
                end
                periodic = Periodic(steps, snaps, store, restore; verbose=info)
                F_opt, F_final, L_opt, L = optcontrol(periodic, steps, adtool)
                @test isapprox(F_opt, F_final, rtol=1e-4)
                @test isapprox(L_opt, L, rtol=1e-4)
            end
        end
    end

    @testset "Test mutable optcontrol" begin
        include("../examples/optcontrol.jl")
        @testset "Testing Revolve..." begin
            global steps = 100
            global snaps = 3
            global info = 0

            revolve = Revolve(steps, snaps; verbose=info)
            F, L, F_opt, L_opt = muoptcontrol(revolve, steps)
            @test isapprox(F_opt, F, rtol=1e-4)
            @test isapprox(L_opt, L, rtol=1e-4)
        end

        @testset "Testing Periodic..." begin
            global steps = 100
            global snaps = 4
            global info = 0

            periodic = Periodic(steps, snaps; verbose=info)
            F, L, F_opt, L_opt = muoptcontrol(periodic, steps)
            @test isapprox(F_opt, F, rtol=1e-4)
            @test isapprox(L_opt, L, rtol=1e-4)
        end
    end
end


# Configuration for running spatial simulation
export simtype
const simtype=2
const num_trials = 2
@everywhere simtype = 2
@everywhere const N_list = [4,8,16,32,64]        # Meta-population size list
const num_attributes_list = [5]        # number attributes for quantitative representation
const N_mut_list = [0.5, 1.0, 2.0, 4.0]
const num_subpops = 1
const ngens = 1       # Generations after burn-in
const burn_in= 2.0    # generations of burn_in as a multiple of N
const ideal = 1.0
const fit_slope = 1.0
const neutral = true


#takes a crsp daily file and makes it weekly
function makeweekly(selectionop::Function = (noopd(x)=x),
    weeklyop::Function = (noopw(x)=x);
    crspnameout=PARAM[:crspweekly],
    binprefix="w_",
    mainpanel::Bool = true,
    computeivol::Bool = PARAM[:computeivol])

  local crsp::DataFrame

  #WARNING- crspd is NOT valid, below is only to create crspw
  crsp = prepcrsp(crsptype=PARAM[:crspdaily],
    csvextension = CSV_EXTENSION,
    incsvstream = IN_CSV_STREAM,
    binprefix = binprefix,
    refreshcrsp=PARAM[:refreshweekly],
    crspcolumns=[PARAM[:weeklycrspcols]; keys(PARAM[:momentumstrategies]) |> collect],
    crsprequiredcolumns=PARAM[:dailycrsprequired]) do crspd

    @assert (PARAM[:crspfrequencysuffix] == "w") || (!mainpanel)
    sort!(crspd, [:permno, :date]) #cache the sort

    #computerangemeasures!(crspd, sorted=true) #not used, mainly for kyle's lamda
    crspd.dvol = computedollarvol(crspd)
    rename!(crspd, :vol=>:svol)
    #crspd.shares = computeshares(crspd)

    #NOTE: testing note- change to trailingsigmatest! to test vol routine
    #println(" checksum: $(sum(skipmissing(crspd.sigma)))")

    crspd = mergeff(crspd, Fgroup=:permno, datefrequency=:day)
    if !issorted(crspd, [:permno, :date])
      sort!(crspd, [:permno, :date])
    end
    #crspd = dailyop(crspd)

    crspw::DataFrame = makeweekly(selectionop, crspd; computeivol)
    validatedates(crspw.date |> unique |> sort!; frequency=:week, validateexactfrequency=true)

    sort!(crspw, [:permno, :date])
    validatesummaryintegrity(crspw, Fid=:weekid)

    if PARAM[:testweeklycsv]
      testpermno::Int = PARAM[:testpermnomult]
      #testcrspw::DataFrame = crspw[crspw.permno .% testpermno .== 0, :]
      testcrspw::DataFrame = crspw[crspw.permno .== 14541, :] |> deepcopy
      #if size(testcrspw,1)>1_000 #prevents scenarios where the file is larger than needed
      #  testcrspw=testcrspw[1:1_000]
      #end

      rename!(testcrspw, :date=>:datew, :dvol=>:dvolw, :ivol=>:ivolw,
        :ret=>:retw, :price=>:pricew, :tridx=>:tridxw,
        :shares=>:sharesw, :mc=>:mcw, :sigma=>:sigmaw,
        :plret=>:plretw, :exchcd=>:exchcdw, :siccd=>:siccdw,)
      rename!(testcrspw, (k->k=>Symbol(k, :w)).(keys(PARAM[:momentumstrategies])))

      #make sure we merge using the same df as used to create the weekyl df
      testcrspout = innerjoin(
        testcrspw, crspd,#view(crspd,completecases(crspd, PARAM[:weeklycrsprequired]),:),
        on=[:permno, :weekid])
      sort!(testcrspout, [:permno, :weekid])
      testcrspout |> CSV.write(
        "$(PARAM[:testpath])\\$(binprefix)dailyforweekly_$(PARAM[:crspdaily]).csv")

    end

    crspw = weeklyop(crspw)

    return crspw
  end #end do block

  #println(describe(crsp))
  return crsp
end

#aggregates a daily dataframe into a weekly dataframe
function makeweekly(selectionop::Function, crspd::DataFrame;
  computeivol::Bool)

  #Assign each date to a weekid
  crspd.weekid = assignweekid(crspd.date)
  crspd.retP1 = 1.0 .+ crspd.ret #convert to gross returns for geometric compounding
  crspd.absret = (abs).(crspd.ret)

  issorted(crspd, [:permno, :date]) || error("crspd must be sorted in makeweekly")
  computeweekend!(crspd)
  trailingsigma!(crspd, Fendpoint=:weekend)
  select!(crspd, Not(:weekend))

  #compute change in week
  computeweekend!(crspd)
  if computeivol
    regressatperiodend!(crspd,
      minpoints=PARAM[:sigmaminpoints],
      maxpoints=PARAM[:sigmamaxpoints], #this is mainly for technical reasons
      calendarinterval = PARAM[:sigmacalendarinterval],
      FXs = PARAM[:ivolfields],
      Fagg = :ivol,
      aggfunc = std,
      Fendpoint = :weekend,
      #=aggfunc = (lm::FMLM)->std(lm.ε)=#)
  else
    @info "ivol not computed"
    crspd.ivol = missings(Float64, nrow(crspd))
  end

  #drop rows with too much missing data and apply selection criteria (e.g. min price, etc)
  @assert issorted(crspd, [:permno, :date])
  oldnrow = nrow(crspd)
  crspd::DataFrame = crspd[completecases(crspd, PARAM[:weeklycrsprequired]), :]
  @assert (size(crspd,1) ≥ oldnrow * 0.5) "weeklycrsprequired dropped more than 50% of rows!!!"

  valid = selectionop(crspd, Ftimegroup=:weekid, datefrequency=:week)
  #valid = combine(groupby(dvalid[!, [:permno, :weekid, ]], [:permno, :weekid]), nrow => :ndays)
  #valid = ensurecompleteness(valid, Ftimegroup=:weekid, datefrequency=:week)
  crspd = innerjoin(crspd, valid, on=[:permno, :weekid], validate=(false,true))

  crspd.plret = adjustindustry(crspd)
  #crspd.plret = Vector{Float64}(crspd.plret)
  crspd.lplret = (log).(crspd.plret .+ 1.0)
  if !issorted(crspd, [:permno, :date])
    sort!(crspd, [:permno, :date])
  end

  momentumstrategies!(crspd, Fconditional=:weekend)
  select!(crspd, Not(:weekend))

  if !issorted(crspd, [:permno, :date])
    sort!(crspd, [:permno, :date])
  end

  #combine by week
  Fmomentums = keys(PARAM[:momentumstrategies])  |> collect
  s2crspds::GroupedDataFrame = groupby(crspd, [:permno, :weekid])
  crspw::DataFrame = combine(s2crspds,
    #:permno => length=> :ndays,
    :ndays => last => :ndays,
    :date => last=> :date,
    :sigma=>last=> :sigma,
    :shares=>last=> :shares,
    #:svol => sum=> :svol,
    :dvol => sum=> :dvol,
    :price => last=> :price,
    :tridx => last => :tridx,
    #:price => mean=> :priceavg,
    :mc => last => :mc,
    :retP1 => prod=> :ret, #WARNING- to be adjusted later
    #:absret=>sum=> :sumabsret,
    :ivol => last=> :ivol,
    :exchcd => last => :exchcd,
    :siccd => last => :siccd,
    :lplret => sum => :plret, #WARNING- to be adjusted later
    Fmomentums .=> last .=> Fmomentums,
    #  :ret => std=> :std,
    #yzstd = [:hi, :lo, :open, :price, :Ldistadjclose]=>yzstd
    )

  crspw.ret .-= 1.0
  crspw.plret .= exp.(crspw.plret) .- 1.0

  testcrspws = groupby(crspw, [:permno, :weekid])
  Threads.@threads for k ∈ keys(s2crspds)
    testscrspw = testcrspws[(permno=k.permno,weekid=k.weekid)]
    @assert sum(log.(1 .+ s2crspds[k].ret)) |> exp ≈ testscrspw.ret[1] + 1.0
  end

  #conform the dates
  scrspws::GroupedDataFrame = groupby(crspw, :weekid)
  Threads.@threads for i ∈ 1:length(scrspws)
    scrspw::SubDataFrame = scrspws[i]
    @assert (maximum(scrspw.date) - minimum(scrspw.date)).value < 7
    scrspw.date .= maximum(scrspw.date)
  end

  return crspw
end

#recommend calling this function whenever we want only the ends of the week, and deleting afterwar
function computeweekend!(crspd::DataFrame; createnew::Bool = true)
  #compute change in week
  (:weekend ∉ names(crspd)) || (error(":weekend already in crspd.
    You probably should have deleted it the last time you used it."))
  crspd.weekend = falses(size(crspd,1))
  @assert issorted(crspd, [:permno, :date])

  scrspds = groupby(crspd, [:permno, :weekid])
  Threads.@threads for i::Int ∈ 1:length(scrspds)
    scrspd::SubDataFrame = scrspds[i]
    scrspd[end, :weekend] = true
  end

  return nothing
end

#assigns a weekid for each date
function assignweekid(dates::AbstractVector{Date}, mindate::Date=Date(1900,1,1))::Vector{Int}
  dayofweek(mindate) == 1 || (
    throw("day of $mindate ≠ 1, so day of week will not align with dayofweek function"))

  maxdate = maximum(dates)
  weekid::Vector{Int} = (d::Date->(d-mindate).value ÷ 7).(dates)

  return weekid
end

avgabs(v::Vector{MFloat64}) = mean((abs).(v))

function validatesummaryintegrity(crsp::DataFrame;
    Fid::Symbol=error("summary id Fid is required"))
  local scrsps::GroupedDataFrame

  #check weekid=>date
  scrsps = groupby(crsp, Fid)
  Threads.@threads for i ∈ 1:length(scrsps)
    scrsp::SubDataFrame = scrsps[i]
    (maximum(scrsp.date) ≠ minimum(scrsp.date)) && (
      error("multiple dates found for 1 Fid $Fid"))
  end

  scrsps = groupby(crsp, :date)
  Threads.@threads for i ∈ 1:length(scrsps)
    scrsp::SubDataFrame = scrsps[i]
    (maximum(scrsp[!, Fid]) ≠ minimum(scrsp[!, Fid])) && (
      error("multiple Fid $Fids found for 1 date"))
  end

  @info "summary data validated using summary id $Fid"
  return nothing
end

function regressatperiodend!(crspd::AbstractDataFrame;
  minpoints::Int = error("minpoints is required"),
  maxpoints::Int = error("maxpoints is required"),
  calendarinterval::DatePeriod = error("calendarinterval is required"),
  Fret::Symbol = :ret,
  Fendpoint::Symbol = error("Fendpoint is required"),
  FXs::Vector{Symbol} =error("FXs are required"),
  aggfunc::Function = error("aggfunc is required"),
  Fagg::Symbol = error("Fagg is required"))

  #local declarations
  local scrspds::GroupedDataFrame

  #this will hold the results
  crspd[!, Fagg] =  Vector{MFloat64}(undef, size(crspd,1))

  #form the regression formula and variable names
  Xexpr::FMExpr = Meta.parse(join((string).(FXs), " + "))
  Xnames::Vector{Symbol} = [:intercept; FXs]
  Yname::Symbol = Fret

  #avoid missing values
  completecrsp::SubDataFrame = view(crspd, completecases(crspd, [Fret; FXs]), :)

  #pre-allocate space before the threaded regressions
  #Since we have variable sized arrays and matrixes, we pre-allocate for each size

  K::Int = length(Xnames)
  Xs::Vector{Matrix{Float64}} = (t::Int->
    Matrix{Float64}(undef, maxpoints, K)).(1:Threads.nthreads())
  ys::Vector{Vector{Float64}} = (t::Int->
    Vector{Float64}(undef, maxpoints)).(1:Threads.nthreads())
  εs::Vector{Vector{Float64}} = (t::Int->
    Vector{Float64}(undef, maxpoints)).(1:Threads.nthreads())

  #first column is the intercept
  for t ∈ 1:Threads.nthreads()
    Xs[t][:, 1] .= ones(maxpoints)
  end

  #helper function to get the residuals
  @inline function resid!(X::AbstractMatrix, y::AbstractVector, m::Vector{Float64}, ε::Vector{Float64})
    ε .= X*m .- y
    return nothing
  end
  #this function gets the beginning of the interval
  @inline getj₀(d::Date, dates::AbstractVector{Date}) = searchsortedfirst(
    dates, d + Day(1) - calendarinterval)

  scrspds = groupby(completecrsp, :permno)
  regressionctr::Vector{Int} = zeros(Int, Threads.nthreads())
  print("Running ivol regressions... ")
  #=@time Threads.@threads =#for i ∈ 1:length(scrspds)
    local scrspd::SubDataFrame = scrspds[i]
    local t::Int = Threads.threadid()

    nrows::Int = size(scrspd,1)
    (nrows < minpoints) && continue

    #loop over possible bounds
    for j::Int ∈ minpoints:nrows
      regressionctr[t] += 1
      #only running regressions at the end of the week
      scrspd[j, Fendpoint] || continue

      #check for stale windows
      j₀::Int = getj₀(scrspd[j, :date], scrspd.date)
      N::Int = j-j₀ +1
      (N < minpoints)  && continue #elliminates windows without enough points

      #get appropriately sized matrices and arrays
      sX::SubArray = view(Xs[t], 1:N, :)
      resize!(ys[t], N)
      resize!(εs[t], N)

      @assert length(ys[t]) == length(εs[t]) == size(sX,1)

      #populate Y vector and X matrix
      ys[t] .= scrspd[j₀:j, Fret]
      for (c, FX) ∈ enumerate(FXs)
        sX[:, c+1] .= scrspd[j₀:j, FX]
      end

      resid!(sX, ys[t], GLM.coef(lm(sX, ys[t])), εs[t])
      scrspd[j, Fagg] = aggfunc(εs[t])
      #scrspd[j, Fagg] = aggfunc(FMLM(Xs[t], ys[t], Xnames=Xnames, Yname=Yname))
    end
  end
  println("$(sum(regressionctr)) regressions complete.")

  return nothing
end


export
    DLADescr,
    DSKDescr,
    EKAttDsc,
    EKSegSum,
    Ellipse,
    center,
    semi_major,
    semi_minor,
    Plane

struct Ellipse
    center::NTuple{3, SpiceDouble}
    semi_major::NTuple{3, SpiceDouble}
    semi_minor::NTuple{3, SpiceDouble}
end

center(e::Ellipse) = collect(e.center)
semi_major(e::Ellipse) = collect(e.semi_major)
semi_minor(e::Ellipse) = collect(e.semi_minor)

struct Plane
    normal::NTuple{3, SpiceDouble}
    constant::SpiceDouble
end

normal(p::Plane) = collect(p.normal)
constant(p::Plane) = p.constant

struct DLADescr
    bwdptr::SpiceInt
    fwdptr::SpiceInt
    ibase::SpiceInt
    isize::SpiceInt
    dbase::SpiceInt
    dsize::SpiceInt
    cbase::SpiceInt
    csize::SpiceInt
end

struct DSKDescr
    surfce::SpiceInt
    center::SpiceInt
    dclass::SpiceInt
    dtype::SpiceInt
    frmcde::SpiceInt
    corsys::SpiceInt
    corpar::NTuple{10, SpiceDouble}
    co1min::SpiceDouble
    co1max::SpiceDouble
    co2min::SpiceDouble
    co2max::SpiceDouble
    co3min::SpiceDouble
    co3max::SpiceDouble
    start::SpiceDouble
    stop::SpiceDouble
end

struct EKAttDsc
    cclass::SpiceInt
    dtype::SpiceInt
    strlen::SpiceInt
    size::SpiceInt
    indexd::SpiceInt
    nullok::SpiceInt
end

struct EKSegSum
    tabnam::NTuple{65, SpiceChar}
    nrows::SpiceInt
    ncols::SpiceInt
    cnames::NTuple{100*33, SpiceChar}
    cdescrs::NTuple{100, EKAttDsc}
end

tabnam(e::EKSegSum) = chararray_to_string(collect(e.tabnam))
cnames(e::EKSegSum) = chararray_to_string(reshape(collect(e.cnames), 33, 100))[1:e.ncols]


devices = Dict{Symbol, DeviceModel}(:Generators => DeviceModel(PSY.ThermalStandard, PSI.ThermalDispatch),
                                    :Loads =>  DeviceModel(PSY.PowerLoad, PSI.StaticPowerLoad))
branches = Dict{Symbol, DeviceModel}(:L => DeviceModel(PSY.Line, PSI.StaticLine),
                                     :T => DeviceModel(PSY.Transformer2W, PSI.StaticTransformer),
                                     :TT => DeviceModel(PSY.TapTransformer , PSI.StaticTransformer))
services = Dict{Symbol, PSI.ServiceModel}()

@testset "Solving ED with CopperPlate" begin
    template = OperationsTemplate(CopperPlatePowerModel, devices, branches, services);
    parameters_value = [true, false]
    systems = [c_sys5, c_sys14]
    test_results = Dict{PSY.System, Float64}(c_sys5 => 240000.0,
                                             c_sys14 => 142000.0)
    for sys in systems, p in parameters_value
        @info("Testing solve ED with CopperPlatePowerModel network")
        @testset "ED CopperPlatePowerModel model use_parameters = $(p)" begin
        ED = OperationsProblem(TestOpProblem, template, sys; optimizer = OSQP_optimizer, use_parameters = p)
        psi_checksolve_test(ED, [MOI.OPTIMAL], test_results[sys], 10000)

        end
    end
end

@testset "Solving ED with PTDF Models" begin
    template = OperationsTemplate(StandardPTDFModel, devices, branches, services);
    parameters_value = [true, false]
    systems = [c_sys5, c_sys14, c_sys14_dc]
    PTDF_ref = Dict{PSY.System, PSY.PTDF}(c_sys5 => PTDF5, c_sys14 => PTDF14, c_sys14_dc => PTDF14_dc)
    test_results = Dict{PSY.System, Float64}(c_sys5 => 340000.0,
                                             c_sys14 => 142000.0,
                                             c_sys14_dc => 142000.0)

    for sys in systems, p in parameters_value
        @info("Testing solve ED with StandardPTDFModel network")
        @testset "ED StandardPTDFModel model use_parameters = $(p)" begin
        ED = OperationsProblem(TestOpProblem, template, sys; PTDF = PTDF_ref[sys], optimizer = OSQP_optimizer, use_parameters = p)
        psi_checksolve_test(ED, [MOI.OPTIMAL], test_results[sys], 10000)
        end
    end
end

@testset "Solving ED With PowerModels with loss-less convex models" begin
    systems = [c_sys5, c_sys14, c_sys14_dc]
    parameters_value = [true, false]
    networks = [DCPPowerModel,
                NFAPowerModel]
    test_results = Dict{PSY.System, Float64}(c_sys5 => 330000.0,
                                             c_sys14 => 142000.0,
                                             c_sys14_dc => 142000.0)

    for  net in networks, p in parameters_value, sys in systems
        @info("Testing solve ED with $(net) network")
        @testset "ED model $(net) and use_parameters = $(p)" begin
        template = OperationsTemplate(net, devices, branches, services);
        ED = OperationsProblem(TestOpProblem, template, sys; optimizer = ipopt_optimizer, use_parameters = p);
        #The tolerance range here is large because NFA has a much lower objective value
        psi_checksolve_test(ED, [MOI.OPTIMAL, MOI.LOCALLY_SOLVED], test_results[sys], 35000)
        end
    end

end

@testset "Solving ED With PowerModels with linear convex models" begin
    systems = [c_sys5, c_sys14]
    parameters_value = [true, false]
    networks = [DCPLLPowerModel,
                LPACCPowerModel]
    test_results = Dict{PSY.System, Float64}(c_sys5 => 340000.0,
                                             c_sys14 => 142000.0,
                                             c_sys14_dc => 142000.0)

    for  net in networks, p in parameters_value, sys in systems
        @info("Testing solve ED with $(net) network")
        @testset "ED model $(net) and use_parameters = $(p)" begin
        template = OperationsTemplate(net, devices, branches, services);
        ED = OperationsProblem(TestOpProblem, template, sys; optimizer = ipopt_optimizer, use_parameters = p);
        #The tolerance range here is large because NFA has a much lower objective value
        psi_checksolve_test(ED, [MOI.OPTIMAL, MOI.LOCALLY_SOLVED], test_results[sys], 10000)

        end
    end

end

#=
@testset "Solving ED With PowerModels with convex SOC and QC models" begin
    systems = [c_sys5, c_sys14]
    parameters_value = [true, false]
    networks = [SOCWRPowerModel,
                 QCRMPowerModel,
                 QCLSPowerModel,]
    test_results = Dict{PSY.System, Float64}(c_sys5 => 320000.0,
                                             c_sys14 => 142000.0)

    for  net in networks, p in parameters_value, sys in systems
        @info("Testing solve ED with $(net) network")
        @testset "ED model $(net) and use_parameters = $(p)" begin
        template = OperationsTemplate(net, devices, branches, services);
        ED = OperationsProblem(TestOpProblem, template, sys; optimizer = ipopt_optimizer, use_parameters = p);
        #The tolerance range here is large because Relaxations have a lower objective value
        psi_checksolve_test(ED, [MOI.OPTIMAL, MOI.LOCALLY_SOLVED], test_results[sys], 25000)

        end
    end

end
=#

@testset "Solving ED With PowerModels Non-Convex Networks" begin
    systems = [c_sys5, c_sys14, c_sys14_dc]
    parameters_value = [true, false]
    networks = [ACPPowerModel,
                #ACRPowerModel,
                ACTPowerModel]
    test_results = Dict{PSY.System, Float64}(c_sys5 => 340000.0,
                                             c_sys14 => 142000.0,
                                             c_sys14_dc => 142000.0)

    for  net in networks, p in parameters_value, sys in systems
        @info("Testing solve ED with $(net) network")
        @testset "ED model $(net) and use_parameters = $(p)" begin
        template = OperationsTemplate(net, devices, branches, services);
        ED = OperationsProblem(TestOpProblem, template, sys; optimizer = ipopt_optimizer, use_parameters = p);
        psi_checksolve_test(ED, [MOI.OPTIMAL, MOI.LOCALLY_SOLVED], test_results[sys], 10000)
        end
    end

end

@testset "Solving UC Linear Networks" begin
    devices = Dict{Symbol, DeviceModel}(:Generators => DeviceModel(PSY.ThermalStandard, PSI.ThermalStandardUnitCommitment),
                                        :Loads =>  DeviceModel(PSY.PowerLoad, PSI.StaticPowerLoad))
    parameters_value = [true, false]
    systems = [c_sys5, c_sys5_dc]
    networks = [DCPPowerModel,
                NFAPowerModel,
                StandardPTDFModel,
                CopperPlatePowerModel]
    PTDF_ref = Dict{PSY.System, PSY.PTDF}(c_sys5 => PTDF5, c_sys5_dc => PTDF5_dc)

    for  net in networks, p in parameters_value, sys in systems
        @info("Testing solve UC with $(net) network")
        @testset "UC model $(net) and use_parameters = $(p)" begin
        template= OperationsTemplate(net, devices, branches, services);
        UC = OperationsProblem(TestOpProblem, template, sys; PTDF = PTDF_ref[sys], optimizer = GLPK_optimizer, use_parameters = p)
        psi_checksolve_test(UC, [MOI.OPTIMAL, MOI.LOCALLY_SOLVED], 340000, 100000)
        end
    end

end


#!/bin/bash
# -*- mode: julia -*-
#=
exec julia --project=@. --color=yes --startup-file=no -e "include(popfirst!(ARGS))" "${BASH_SOURCE[0]}" "$@"
=#

using JuMP
import Gurobi

function alternating()
    m = direct_model(Gurobi.Optimizer())
    @variable(m, x[1:100000] >= 0, Int)
    for e in x
        unset_integer(e)
        is_integer(e)
        lower_bound(e)
    end
    return
end

function batch()
    m = direct_model(Gurobi.Optimizer())
    @variable(m, x[1:100000] >= 0, Int)
    unset_integer.(x)
    is_integer.(x)
    lower_bound.(x)
    return
end

struct SavedVarConf
  was_bin :: Bool
  was_int :: Bool
  was_fixed :: Bool
  fix_value :: Float64
  had_lb :: Bool
  lb :: Float64
  had_ub :: Bool
  ub :: Float64
end

function SavedVarConf(var :: VariableRef) :: SavedVarConf
  was_bin = is_binary(var)
  was_int = is_integer(var)
  was_fixed = is_fixed(var)
  had_lb = has_lower_bound(var)
  had_ub = has_upper_bound(var)
  return SavedVarConf(
    was_bin, was_int,
    was_fixed, (was_fixed ? fix_value(var) : 0.0),
    had_lb, (had_lb ? lower_bound(var) : 0.0),
    had_ub, (had_ub ? upper_bound(var) : 0.0)
  )
end

function relax!(var :: VariableRef) :: VariableRef
  if is_integer(var)
    unset_integer(var)
  elseif is_binary(var)
    unset_binary(var)
    if !has_lower_bound(var) || lower_bound(var) < 0.0
      set_lower_bound(var, 0.0)
    end
    if !has_upper_bound(var) || upper_bound(var) < 0.0
      set_upper_bound(var, 1.0)
    end
  end

  return var
end

function save_and_relax!(var :: VariableRef)
  svc = SavedVarConf(var)
  if svc.was_int
    unset_integer(var)
  elseif svc.was_bin
    unset_binary(var)
    if !svc.had_lb || svc.lb < 0.0
      set_lower_bound(var, 0.0)
    end
    if !svc.had_ub || svc.ub > 1.0
      set_upper_bound(var, 1.0)
    end
  end

  return svc
end

function alternating2()
    m = direct_model(Gurobi.Optimizer())
    @variable(m, x[1:100000] >= 0, Int)
    save_and_relax!.(x)
    return
end

function batch2()
    m = direct_model(Gurobi.Optimizer())
    @variable(m, x[1:100000] >= 0, Int)
    SavedVarConf.(x)
    relax!.(x)
    return
end

function test()
    println("-------------------- batch --------------------------")
    @time batch()
    println("-------------------- alternating --------------------")
    @time alternating()
    #=
    println("-------------------- batch2 -------------------------")
    @time batch2()
    println("-------------------- alternating2 -------------------")
    @time alternating2()
    =#
end

test()
#println("-------------------- second run ---------------------")
#test()


struct AhoCorasickAutomaton{T <: Unsigned}
    base::Vector{T}
    from::Vector{T}
    ikey::Vector{T}
    deep::Vector{T}
    back::Vector{T}
    arcs::Vector{Vector{UInt8}}
end

function AhoCorasickAutomaton{T}() where T
    base = T[1]
    from = T[1]
    ikey = T[0]
    deep = T[0]
    back = T[0]
    arcs = [UInt8[]]
    return AhoCorasickAutomaton{T}(base, from, ikey, deep, back, arcs)
end

function AhoCorasickAutomaton{T}(keys::Vector, sort::Bool) where T
        obj = AhoCorasickAutomaton{T}()
        if sort && !issorted(keys) Base.sort!(keys) end
        for (key, i) in keys
                addkey!(obj, codeunits(key), T(i))
        end
        shrink!(obj)
        @inbounds fillback!(obj)
        validate(obj)
        resize!(obj.arcs, 0)
        return obj
end

function AhoCorasickAutomaton{T}(keys::AbstractDict{String, Ti}; sort::Bool = true) where {T, Ti}
        return AhoCorasickAutomaton{T}(collect(keys), sort)
end

function AhoCorasickAutomaton{T}(keys::Vector{String}; sort::Bool = true) where T
    return AhoCorasickAutomaton{T}(collect(zip(keys, 1:length(keys))), sort)
end

function shrink!(obj::AhoCorasickAutomaton{T})::T where T
    actlen = findlast(!iszero, obj.from)
    if (actlen < length(obj.from))
        resize!(obj.base, actlen)
        resize!(obj.from, actlen)
        resize!(obj.ikey, actlen)
        resize!(obj.deep, actlen)
        resize!(obj.back, actlen)
        resize!(obj.arcs, actlen)
    end
    return actlen
end

function enlarge!(obj::AhoCorasickAutomaton{T}, newlen::T)::T where T
    base = obj.base; from = obj.from; deep = obj.deep; back = obj.back; ikey = obj.ikey; arcs = obj.arcs;
    oldlen::T = length(obj.base)
    newlen2 = oldlen
    while newlen2 < newlen newlen2 *= 2 end
    if (oldlen < newlen2)
        resize!(base, newlen2)
        resize!(from, newlen2)
        resize!(ikey, newlen2)
        resize!(deep, newlen2)
        resize!(back, newlen2)
        resize!(arcs, newlen2)
        # for i = oldlen + 1:newlen2 base[i] = i end
        base[oldlen + 1:newlen2] .= 0
        from[oldlen + 1:newlen2] .= 0
        ikey[oldlen + 1:newlen2] .= 0
        deep[oldlen + 1:newlen2] .= 0
        back[oldlen + 1:newlen2] .= 0
        for i in oldlen + 1:newlen2
            arcs[i] = UInt8[]
        end
    end
    return newlen2
end

function addkey!(obj::AhoCorasickAutomaton{T}, code::Base.CodeUnits{UInt8,String}, icode::T)::Nothing where T
    base = obj.base; from = obj.from; deep = obj.deep; back = obj.back; ikey = obj.ikey; arcs = obj.arcs;
    cur::T = 1
    nxt::T = 0
    for c in code
        nxt = base[cur] + c
        enlarge!(obj, nxt)
        if (from[nxt] == 0)
            from[nxt] = cur
            push!(arcs[cur], c)
            deep[nxt] = deep[cur] + 1
            cur = nxt
        elseif (from[nxt] == cur)
            cur = nxt
        else # from[nxt] != cur
            push!(arcs[cur], c)
            if length(arcs[cur]) <= length(arcs[from[nxt]]) || from[nxt] == from[cur]
                rebase!(obj, cur)
                nxt = base[cur] + c
            else
                rebase!(obj, from[nxt])
            end
            from[nxt] = cur
            deep[nxt] = deep[cur] + 1
            cur = nxt
        end
    end
    ikey[cur] = icode
    return nothing
end

function rebase!(obj::AhoCorasickAutomaton{T}, cur::T)::Nothing where T
    base = obj.base; from = obj.from; deep = obj.deep; back = obj.back; ikey = obj.ikey; arcs = obj.arcs;
    oldbase = base[cur]
    @assert length(arcs[cur]) > 0 string(cur)
    newbase = findbase(obj, cur)
    enlarge!(obj, newbase + maximum(arcs[cur]))
    for i = eachindex(arcs[cur])
        # arc = arcs[cur][i]
        newson = newbase + arcs[cur][i]
        from[newson] = cur
        oldson = oldbase + arcs[cur][i]
        if (from[oldson] != cur) continue end
        base[newson] = base[oldson]
        ikey[newson] = ikey[oldson]
        deep[newson] = deep[oldson]
        z = arcs[newson]; arcs[newson] = arcs[oldson]; arcs[oldson] = z;
        # grandsons
        for arc in arcs[newson]
            from[base[newson] + arc] = newson
        end
        # oldson
        base[oldson] = from[oldson] = ikey[oldson] = deep[oldson] = 0
    end
    base[cur] = newbase
    return nothing
end

function findbase(obj::AhoCorasickAutomaton{T}, cur::T)::T where T
    base = obj.base; from = obj.from; deep = obj.deep; back = obj.back; ikey = obj.ikey; arcs = obj.arcs;
    n::T = length(from)
    for b = max(cur + 1, base[cur]):n
        ok = true
        for arc in arcs[cur]
            @inbounds ok &= arc + b > n || from[arc + b] == 0
        end
        if (ok)
            return b
        end
    end
    return T(n + 1)
end

function fillback!(obj::AhoCorasickAutomaton{T})::Nothing where T
    base = obj.base; from = obj.from; deep = obj.deep; back = obj.back; ikey = obj.ikey; arcs = obj.arcs;
    #println(arcs)
    n::T = length(arcs)
    root::T = 1
    que = similar(base); head::T = 1; tail::T = 2;
    que[1] = root; back[root] = root;
    while head < tail
        cur = que[head]; head += 1;
        for arc in arcs[cur]
            chd = base[cur] + arc
            chdback = root
            if (cur != root)
                chdback = back[cur]
                while chdback != root && (base[chdback] + arc > n || from[base[chdback] + arc] != chdback)
                    chdback = back[chdback]
                end
                if base[chdback] + arc <= n && from[base[chdback] + arc] == chdback
                    chdback = base[chdback] + arc
                end
            end
            back[chd] = chdback
            que[tail] = chd; tail += 1;
        end
    end
    return nothing
end

function validate(obj::AhoCorasickAutomaton{T})::Nothing where T
    base = obj.base; from = obj.from; deep = obj.deep; back = obj.back; ikey = obj.ikey; arcs = obj.arcs;
    root = 1
    que = similar(base); head = 1; tail = 2;
    que[1] = root;
    while head < tail
        cur = que[head]; head += 1;
        for arc in arcs[cur]
            chd = base[cur] + arc
            # @assert from[chd] == cur && back[chd] != chd && back[chd] != 0 string(chd, " fa=", cur, " from=", from[chd], " back=", back[chd])
            @assert from[chd] == cur string("cur=", cur, " chd=", chd, " from=", from[chd])
            que[tail] = chd; tail += 1;
        end
    end
    return nothing
end

"""
ACMatch has 3 fields:

    1. s : start of match
    2. t : stop of match, [s, t), using str[s:prevind(str, t)] to get matched patterns
    3. i : index of the key in *obj*, which is the original insertion order of keys to *obj*

The field *i* may be use as index of external property arrays, i.e., the AhoCorasickAutomaton
can act as a `Map{String, Any}`.
"""
struct ACMatch
    s::Int
    t::Int
    i::Int
end

import Base.length
length(x::ACMatch) = x.t - x.s

function isless(x::ACMatch, y::ACMatch)::Bool
    return x.s < y.s || x.s == y.s && x.t < y.t || x.s == y.s && x.t == y.t && x.i < y.i
end

function eachmatch(obj::AhoCorasickAutomaton{T}, text::AbstractString)::Vector{ACMatch} where T
    return eachmatch(obj, codeunits(text))
end

function eachmatch(obj::AhoCorasickAutomaton{T}, codes::DenseVector{UInt8})::Vector{ACMatch} where T
    base = obj.base; from = obj.from; deep = obj.deep; back = obj.back; ikey = obj.ikey; arcs = obj.arcs;
    n = length(base)
    root = cur = T(1)
    res = ACMatch[]
    for i = 1:length(codes)
        c = codes[i]
        while cur != root && (base[cur] + c > n || from[base[cur] + c ] != cur)
            cur = back[cur]
        end
        if (base[cur] + c <= n && from[base[cur] + c] == cur)
            cur = base[cur] + c
        end
        # if (ikey[cur] > 0)
            node = cur
            while node != root
                if (ikey[node] > 0) push!(res, ACMatch(i + 1 - deep[node], i + 1, ikey[node])) end
                node = back[node]
            end
        # end
    end
    return res
end

#### utils
import Base.getindex
getindex(xs::String, match::ACMatch) = xs[match.s:prevind(xs, match.t)]

function get(obj::AhoCorasickAutomaton{T}, key::DenseVector{UInt8}, default::T)::T where T
    cur::T = 1
    n::T = length(obj.from)
    for c in key
        nxt = obj.base[cur] + c
        if (nxt <= n && obj.from[nxt] == cur)
            cur = nxt
        else
            return default
        end
    end
    return obj.ikey[cur]
end

function get(obj::AhoCorasickAutomaton{T}, key::AbstractString, default::T)::T where T
    return get(obj, codeunits(key), default)
end

getindex(aca::AhoCorasickAutomaton{T}, key::AbstractString) where {T} = get(aca, key, T(0))

function in(key::AbstractString, obj::AhoCorasickAutomaton{T})::Bool where T
    return get(obj, key, T(0)) > 0
end

function in(key::DenseVector{UInt8}, obj::AhoCorasickAutomaton{T})::Bool where T
    return get(obj, key, T(0)) > 0
end

function length(obj::AhoCorasickAutomaton{T}) where T
    return count(!iszero, obj.ikey)
end

function collect(obj::AhoCorasickAutomaton{T}) where T
    base = obj.base
    from = obj.from
    ikey = obj.ikey
    res = Pair{String, Int}[]
    for i = 1:length(ikey)
        if ikey[i] == 0 continue end
        codes = UInt8[]
        j = i
        while j > 1
            c = j - base[from[j]]
            push!(codes, c)
            j = from[j]
        end
        push!(res, String(reverse!(codes)) => ikey[i])
    end
    return res
end

function keys(obj::AhoCorasickAutomaton{T}) where T
    return map(first, collect(obj))
end

function values(obj::AhoCorasickAutomaton{T}) where T
    return filter(!iszero, obj.ikey)
end

function ==(x::AhoCorasickAutomaton, y::AhoCorasickAutomaton)
    return x.base == y.base && x.from == y.from && x.ikey == y.ikey && x.deep == y.deep && x.back == y.back
end

function show(io::IO, o::AhoCorasickAutomaton{T}) where T
    print(io, "AhoCorasickAutomaton{$T}", "(keys=", length(o), ")")
end