"""
    package ModiaMedia

Media models for use with Modia and other Julia packages.
It is planned that this package contains media property models to be used in package
[Modia](https://github.com/ModiaSim/Modia.jl), but also in other Julia packages.
The initial goal is to achieve a similar functionality as
[Modelica.Media](https://doc.modelica.org/Modelica%203.2.3/Resources/helpDymola/Modelica_Media.html#Modelica.Media),
the standard media library for Modelica models, but with improvements based on Julia's features
such as multiple dispatch.

A medium model is basically a struct that has the following structure:

```
mutable struct MediumName <: AbstractMedium
    infos::ModiaMedium.FluidInfos
    fluidConstants::SVector{1,ModiaMedium.AbstractFluidConstants}
    fluidLimits::ModiaMedium.FluidLimits
    data::MediumSpecificData
end
```

and functions operating on instances of such a struct.

This package is currently under development.
"""
module ModiaMedia

const path    = dirname(dirname(@__FILE__))          # Absolute path of package directory
const Version = "0.1.0-dev"
const Date    = "2019-02-25"

println(" \nImporting ModiaMedia Version $Version ($Date)")

# Export Abstract types
export AbstractMedium
export PureSubstance
export MixtureMedium
export CondensingGases
export ThermodynamicState
export MixtureThermodynamicState
export AbstractFluidConstants

# Export types used in a Medium
export FluidInfos
export FluidLimits
export BasicFluidConstants
export IdealGasFluidConstants

# Export State structs
export SimpleMediumState
export SingleGasNasaState
export SimpleIdealGasMediumState
export MoistAirState

# Export general functions
export getMedium, listMedia, standardCharacteristics, standardPlot

# Export set state functions
export setState_pTX, setState_pTX!, setState_pT, setState_pT!
export setState_phX, setState_phX!, setState_ph, setState_ph!
export setState_psX, setState_psX!, setState_ps, setState_ps!
export setState_dTX, setState_dTX!, setState_dT, setState_dT!
export isenthalpicState, isenthalpicState!

# Export thermodynamic property functions for all media
export density, density_phX, density_pTX, density_der_1, density_pT, density_pT_der_1, density_pT_der_2, density_pT_der_3
export specificInternalEnergy_T, specificInternalEnergy_T_der_1, specificInternalEnergy_T_der_2
export temperature, temperature_phX, temperature_ph
export pressure, pressure_dT
export specificEnthalpy, specificEnthalpy_pTX, specificEnthalpy_dT, specificEnthalpy_T
export specificInternalEnergy, specificHeatCapacityCp
export dynamicViscosity
export gasConstant

# Export PureSubstance thermodynamic property  functions
export density_ph, temperature_ph, pressure_dT, specificEnthalpy_dT

# Export MoistAir specific functions
export saturationPressureOfLiquidWater
export sublimationPressureIce
export saturationPressureOfWater

# Enumeration values
export IndependentVariables, IndependentVariables_T, IndependentVariables_pT, IndependentVariables_ph
export IndependentVariables_phX, IndependentVariables_pT, IndependentVariables_dTX
export ReferenceEnthalpy, ReferenceEnthalpy_ZeroAt0K, ReferenceEnthalpy_ZeroAt25C, ReferenceEnthalpy_UserDefined
export ReferenceEntropy , ReferenceEntropy_ZeroAt0K , ReferenceEntropy_ZeroAt0C  , ReferenceEntropy_UserDefined

### Abstract types -------------------------------------------------------------------------------
# The following structures and names are identical to Modelica.Media.Interfaces
# with the only exception, that "Partial" is replaced by "Abstract"

"""
    abstract type AbstractMedium

Abstract type of all media.
"""
abstract type AbstractMedium end

"""
    abstract type PureSubstance <: AbstractMedium

Abstract type of all media consisting of a pure substance.
"""
abstract type PureSubstance <: AbstractMedium end

"""
    abstract type MixtureMedium <: AbstractMedium

Abstract type of all media consisting of a mixture of media.
"""
abstract type MixtureMedium <: AbstractMedium end

"""
    abstract type CondensingGases <: AbstractMedium

Abstract type of all media consisting of condensing media.
"""
abstract type CondensingGases <: MixtureMedium end

"""
   abstract type ThermodynamicState

Abstract type of all media states.
"""
abstract type ThermodynamicState end

"""
    abstract type MixtureThermodynamicState <: ThermodynamicState

Abstract type of the states of all media consisting of a mixture of media.
"""
abstract type MixtureThermodynamicState <: ThermodynamicState end

"""
    abstract type AbstractFluidConstants

Abstract type of all FluidConstants structures.
"""
abstract type AbstractFluidConstants end

### Importing packages -------------------------------------------------------------------------------
using  JSON
using  StaticArrays
using  Unitful
import DataFrames
import ModiaMath
import Serialization

### Including files for the ModiaMedia module --------------------------------------------------------
include("Interfaces/PartialMedium.jl")
include("Interfaces/PartialPureSubstance.jl")
include("Interfaces/PartialMixtureMedium.jl")
include("Interfaces/PartialCondensingGases.jl")

include("Media/SimpleMedium.jl")
include("Media/SimpleIdealGasMedium.jl")
include("Media/SingleGasNasa.jl")
include("Media/MoistAir.jl")

const mediumDictFile   = "$path/src/Media/media.julia_serializer"
const generateDictFile = "$path/dict/GenerateMediumDict.jl"
const mediumDict       = Array{Any}(nothing,1)

### Load medium dictionary from file
function loadMediumDictFile()
    global mediumDictFile
    println("... Read media dictionary from file:\n",
            "    ", mediumDictFile)
    f = open(mediumDictFile)
    dict = Serialization.deserialize(f)
    close(f)
    return dict
end

function loadMediumDict()
    global generateDictFile

    if isfile(mediumDictFile)
        try
            dict = loadMediumDictFile()
        catch
            println("... Regenerate media dictionary")
            include(generateDictFile)
            dict = loadMediumDictFile()
        end
    else
        include(generateDictFile)
        dict = loadMediumDictFile()
    end

    return dict
end

"""
    Medium = getMedium(name::AbstractString)

Return `Medium` object from medium `name`.
Possible values of argument `name` can be inquired via `listMedia()`
([Available media](@ref)).

# Examples
```julia
medium = getMedium("SimpleAir")
```

"""
function getMedium(name::AbstractString)::AbstractMedium
    global mediumDict
    if typeof(mediumDict[1]) == Nothing
        mediumDict[1] = loadMediumDict()
    end
    return mediumDict[1][name]
end

"""
    listMedia()

List available media of ModiaMedia.
"""
function listMedia()::Nothing
    global mediumDict
    if typeof(mediumDict[1]) == Nothing
        mediumDict[1] = loadMediumDict()
    end

    dict = mediumDict[1]
    media_table = DataFrames.DataFrame(name=AbstractString[], type=Symbol[])

    for key in sort(collect(keys(dict)))
        push!(media_table, [key, Symbol(typeof(dict[key]))])
    end

    println("\nMedia available in ModiaMedia:\n")
    show(media_table,allrows=true,allcols=true,summary=false)
    return nothing
end

# Import packages that are used in examples and tests
# (in order that there are no requirements on the environment
#  in which the examples and tests are executed).
import JSON
import ModiaMath
import Serialization
import StaticArrays
import Unitful
import Test

end # module


using ImageView, Images   # TestImages  just a reminder poop stinks!
using Gtk.ShortNames

grid, frames, canvases = canvasgrid((1,1))  # 1 row, 2 columns

#
#img = load("/home/travis/Documents/Mael Engine/sprites/cloud.png")
cloud = load("/home/travis/Documents/Mael Engine/sprites/cloud.png")
cloud2 = load("/home/travis/Documents/Mael Engine/sprites/cloud2.png")
LeftPlayer = load("/home/travis/Documents/Mael Engine/sprites/leftplayer.png")
MiddlePlayer = load("/home/travis/Documents/Mael Engine/sprites/middleplayer.png")
img = fill(RGBA(0.65625,0.796875,1,1), 500, 800)

imshow(canvases[1,1], img )
#win = @Window(canvas, "Grinding Gears", 500, 800)

win = Window(grid, "Grinding Gears", 800, 500)
showall(win)
# ==============================================================================
# This draws a line to a picture!
# ==============================================================================
function line(image, x1, y1, x2, y2, color)
        dx, dy = x2-x1,  y2-y1

        for x in x1:x2
              y = y1 + dy * (x - x1) / dx
              image[ round(Int32, y), round(Int32, x) ] = color
        end
end
# ==============================================================================
# This draws a rectrangle to a picture!
# ==============================================================================
function rect(image, left, top, width, height, color)
    for y in 1:height, x in 1:width
        pixel(image, top+y, left+x, color )
    end
end
# ==============================================================================
function clearScreen(image, color)
    height, width = size(image)
    for y in 1:height, x in 1:width
        image[ y, x ] = color
    end
end
# ==============================================================================
# This draws a rectrangle to a picture! 1="centered" 2= topCenter
# ==============================================================================
function drawSprite(image, sprite, x, y, position=1)
    # width, height = size(image)
    SpHeight, SpWidth = size(sprite)
    if position==1
        cx,cy = round(Int32, SpWidth/2), round(Int32, SpHeight/2)
    elseif  position==2
        cx,cy = round(Int32, SpWidth/2), SpHeight
    elseif position==3
        cx,cy = round(Int32, SpWidth/2), 0
    end
    left, top = x-cy, y-cx

    for y in 1:SpWidth,  x in 1:SpHeight
        pixel(image, left+x, top+y, sprite[ x, y ] )
    end
end
# ==============================================================================
function drawSprite(image, sprite) # sprite.image, sprite.x, sprite.y, sprite.position=1
    SpHeight, SpWidth = size(sprite.image)
    if sprite.position==1
        cx,cy = round(Int32, SpWidth/2), round(Int32, SpHeight/2)
    elseif  sprite.position==2
        cx,cy = round(Int32, SpWidth/2), SpHeight
    elseif sprite.position==3
        cx,cy = round(Int32, SpWidth/2), 0
    end
    left, top =  sprite.y-cy, sprite.x-cx

    for y in 1:SpWidth,  x in 1:SpHeight
        pixel(image, left+x, top+y, sprite.image[ x, y ] )
    end
end
# ==============================================================================
# This draws a pixel with Alpha chanel
# ==============================================================================
function pixel(image, X, Y, color )
    a = alpha(color)
    width, height = size(image)
    if X>0 && X < width && Y>0 && Y < height
        image[ X,Y ] = image[ X,Y ]*(1-a) + (color*a)
    end
end
# ==============================================================================
# This calculates the positions of all sprites draws them.
# ==============================================================================
function Paint(spritList)
     height, width  = size(img)
    clearScreen(img, RGBA(0.65625,0.796875,1,1))
    for sprit in spritList
        drawSprite(img, sprit)
        if sprit.direction == 0
        elseif sprit.direction == 1
            if sprit.x < width
                sprit.x += 4
            else
                sprit.direction =2
            end
        elseif sprit.direction == 2
            if sprit.x > 0
                sprit.x -= 4
            else
                sprit.direction = 1
            end
        elseif sprit.direction == 3
            if sprit.y < height
                sprit.y += 4
            else
                sprit.direction =4
            end
        elseif sprit.direction == 4
            if sprit.y > 0
                sprit.y -= 4
            else
                sprit.direction = 3
            end
        end
    end
    imshow(canvases[1,1], img)
end
# ==============================================================================
# Data structure to hold sprites
# ==============================================================================
mutable struct Sprite
    image           # Image or image class. for example it could be mario but mario could change depending on what he is doing
    x::Int          # Coordinates
    y::Int          # Coordinates
    direction::Int  # direction of movement.
    movement::Int   # Type of movement: walk, jump, float, fall...
    position::Int   # the positioning: centered, above, below...
end
# ==============================================================================

spritelist = []

push!(spritelist, Sprite(cloud, 100, 100, 1, 3, 2) )
push!(spritelist, Sprite(cloud, 100, 600, 1, 3, 2) )
push!(spritelist, Sprite(cloud2, 100, 400, 2, 3, 2) )
push!(spritelist, Sprite(cloud, 150, 200, 1, 3, 2) )

push!(spritelist, Sprite(LeftPlayer, 100, 150, 1, 3, 2) )
push!(spritelist, Sprite(LeftPlayer, 200, 150, 1, 3, 2) )
# mario = Sprite(LeftPlayer, 100, 150, 1, 3, 2)
# push!(spritelist, mario)

# key events in GTK -> gdk.jl
const Left = 0xff51
const Up = 0xff52
const Right = 0xff53
const Down = 0xff54

signal_connect(win, "key-press-event") do widget, event
    if event.keyval == Left
        spritelist[5].direction = 2
    elseif event.keyval == Up
        spritelist[5].direction = 4
    elseif event.keyval == Right
        spritelist[5].direction = 1
    elseif event.keyval == Down
        spritelist[5].direction = 3
    end
    if event.keyval == 97
        spritelist[6].direction = 2
    elseif event.keyval == 119
        spritelist[6].direction = 4
    elseif event.keyval == 100
        spritelist[6].direction = 1
    elseif event.keyval == 115
        spritelist[6].direction = 3
    end
    print(event.keyval)
end
signal_connect(win, "key-release-event") do widget, event
    #c = Char(event.keyval)
 # println("You released key $(c)") #.keyval
end

u1 = Timer(u1 -> Paint(spritelist), 0.0003, 0.001)


#Copyright (c) 2015 Rex Computing and Isaac Yonemoto

#see LICENSE.txt

#this work was supported in part by DARPA Contract D15PC00135

#fracwords-speed.jl
#testing the speed of various ways of doing fracwords

const count = 10000000

function fracwords(n::UInt16)
  n < 6 ? 1 : (1 << (n - 6))
end

#enclose in a function body because in julia there are no constant type globals.
function testy(n::UInt16)
println("int(ceil(2^n / 64)): $(int(ceil(2^n/64)))")
res = 0
tic()
for(i=1:count)
  res = int(ceil(2^n/64))
end
toc()
#println forces julia to use the result and not compile it away.
println(res)

println("fracwords(n): $(fracwords(n))")
tic()
for(i=1:count)
  res = fracwords(n)
end
toc()
println(res)

println("direct n > 6 ? 1 : (1 << (n - 6)): $((1 << (n-6)) + 1)")
tic()
for(i=1:count)
  res = n < 6 ? 1: (1 << (n - 6))
end
toc()
println(res)

println("----")
end

testy(UInt16(1))
testy(UInt16(1))
testy(UInt16(1))
testy(UInt16(0))
testy(UInt16(7))
testy(UInt16(10))

#20 august 2015
#fixed code, ran again.  Note that about 10x more runs are being done now.
#results run on Laptop (Core3 2 Ghz x 2)

#int(ceil(2^n / 64)): 1
#elapsed time: 0.145344527 seconds
#1
#fracwords(n): 1
#elapsed time: 0.004367947 seconds
#1
#direct (1 << (n - 6)): 1
#elapsed time: 0.004367371 seconds
#1
#----
#int(ceil(2^n / 64)): 1
#elapsed time: 0.145334126 seconds
#1
#fracwords(n): 1
#elapsed time: 0.004361422 seconds
#1
#direct (1 << (n - 6)): 1
#elapsed time: 0.004363537 seconds
#1
#----
#int(ceil(2^n / 64)): 1
#elapsed time: 0.145350655 seconds
#1
#fracwords(n): 1
#elapsed time: 0.004363412 seconds
#1
#direct (1 << (n - 6)): 1
#elapsed time: 0.004392828 seconds
#1
#----
#int(ceil(2^n / 64)): 1
#elapsed time: 0.14566474 seconds
#1
#fracwords(n): 1
#elapsed time: 0.004361116 seconds
#1
#direct (1 << (n - 6)): 1
#elapsed time: 0.004361952 seconds
#1
#----
#int(ceil(2^n / 64)): 2
#elapsed time: 0.229761836 seconds
#2
#fracwords(n): 2
#elapsed time: 0.004361318 seconds
#2
#direct (1 << (n - 6)): 3
#elapsed time: 0.004363907 seconds
#2
#----
#int(ceil(2^n / 64)): 16
#elapsed time: 0.203764237 seconds
#16
#fracwords(n): 16
#elapsed time: 0.00436676 seconds
#16
#direct (1 << (n - 6)): 17
#elapsed time: 0.00436218 seconds
#16
#----

#results:  Addind an extra function call seems to not really make a difference,
#while making it far less likely something wonky will happen.  Additionally,
#an extra conditional for small numbers gives the correct answers, so...  Use that.


# License for this file: MIT (expat)
# Copyright 2017-2018, DLR Institute of System Dynamics and Control
#
# This file is part of module
#   ModiaMath.Variables (ModiaMath/Variables/_module.jl)
#

# Definitions of Real variables

#=
The dynamic system is described as index-1 DAE:

    wr := wr(der(x),x,t)      # Additional variables, that are explicitly computed but are not reported to the integrator (if used in fc(...), wr is not allowed to depend on der(x)
    0   = fd(der(x),x,t,wr)
    0   = fc(x,t,wr)
    wc := wc(der(x),x,t,wr)   # Additional variables, that need to be only computed at communication points.

such that (assuming wr is inlined in fd/fc)

   [der(fd, der(x));
    der(fc, x)     ] is regular

The vectors are partitioned as:

   x      = [xd_exp;        # XD_EXP: variables appearing differentiated in the model and the model explicitly computes der(xd_exp), so der(xd_exp) is an output of the model (used in model)
             xd_imp;        # XD_IMP: variables appearing differentiated in the model and the model does not compute der(xd_imp), so der(xd_imp) is an input to the model (used in model)
             xa;            # XA    : variables not appearing differentiated in the model, so algebraic variables (used in model)
             lambda_int;    #         integral of index-1 algebraic variables (not used in model; used only by integrator)
             mue_int   ]    #         integral of stabilizing variables der(mue_int) (not used in model; used only by integrator)

   der(x) = [der(xd_exp);   # DER_XD_EXP: derivative of xd_exp; is computed explicitly in the model (used in model).
             der(xd_imp);   # DER_XD_IMP: derivative of xd_imp; is provided by integrator (used in model)
             der(xa);       #             derivative of xa (not used in model; used only in integrator)
             lambda;        # LAMBDA    : index-1 algebraic variable; is provided by integrator (used in model)
             mue        ]   # MUE       : stabilizing variable to reduce higher index to index 1; is provided by integrator (used in model)

   residue = [fd_exp;       #           : residue of explicitly solved fd equations; computed by code below (e.g. if x[1] = phi and x[2] = v (= der(phi)), then residue[1] = derx[1] - v)
              fd_imp;       # FD_IMP    : residue of implicitly solved fd equations (is provided by model)
              fc]           # FC        : residue of fc equations (is provided by model)

If x consists only of xd_exp, then the DAE is an ODE and an ODE integration method can be used.

All variables x and der(x) are subtypes of type Real (typically Float64).
These variables are below defined as subtypes of type ContinuousTimeVariable

Types WR are auxiliary variables that are explicitly computed as functions of t,x,der(x) and are used to compute residues
(so these variables are computed at every call of the model)

Types WC are additional variables that are computed only at communication points.

Type TIME is just used to mark which variable is the independent time variable.
=#

isNothing(T) = typeof(T) == NOTHING
hasValue(T)  = typeof(T) != NOTHING

#------------------------------ Generic Real Variable ----------------------------------------
"""
    v = RealVariable{ValueType,ElementType}(...)

Generate a new variable with `v.value::ValueType`, `v.nominal::ElementType`.
The argument list is described in module [`Variables`](@ref).
"""
mutable struct RealVariable{ValueType,ElementType} <: ModiaMath.AbstractRealVariable
    # Generic Attributes of every AbstractVariable (partially based on FMI 2.0)
    _internal::ComponentInternal
    value::ValueType                 # Actual value of variable (can be a scalar or an array)
    info::AbstractString
    causality::Causality
    variability::Variability
    start::ValueType                 # Initial value of variable
    fixed::Bool                      # = true, start is fixed during initialization; = false, start is a guess value (can be changed during initialization)
    analysis::VariableAnalysisType   # Analysis for which the Variable is used

    # Attributes specific to Real variables
    min::ElementType
    max::ElementType
    nominal::ElementType                                              # nominal value; is used to compute absolute tolerances and might be used for scaling
    flow::Bool
    numericType::NumericType                                          # how the variable is used in the equations
    integral::Union{NOTHING,RealVariable{ValueType,ElementType}}  # if present, integral is the variable that represents the integral of the actual variable (so variable = d(integral)/dt     derivative::Union{NOTHING, RealVariable{ValueType, ElementType}}  # if present, derivative is the variable that represents the derivative of the actual variable (so derivative = d(variable)/dt)
    derivative::Union{NOTHING,RealVariable{ValueType,ElementType}}  # if present, derivative is the variable that represents the derivative of the actual variable (so derivative = d(variable)/dt)
    unit::String                                                      # unit of the variable (temporal solution until package Unitful is supported in Julia v0.7)

    # How the variable is stored in vectors
    ivar::Int        # if value is stored in x/derx/residue vector: x/derx/residue[ ivar ] = first value of variable.value
    iresult::Int     # if value is stored in result vector: result[ iresult ] = first value of variable.value

    function RealVariable{ValueType,ElementType}(name, within, info, start, unit, fixed, min, max, nominal, flow, causality, variability,
      numericType, integral, analysis) where {ValueType, ElementType}

        variable = new(ComponentInternal(name, within), deepcopy(start), info, causality, variability,
                     start, fixed, analysis, min, max, nominal, flow, numericType, integral, nothing, unit, 0, 0)

        if typeof(within) != NOTHING
            setfield!(within, Symbol(name), variable)
        end

        if hasValue(integral)
            integral.derivative = variable
        end

        return variable
    end
end

RealVariable{ValueType,ElementType}(name=NoNameDefined,
    within=nothing;
    info="",
    start=nothing,
    unit=NoUnit,
    fixed=false,
    min=-ElementType(Inf),
    max=ElementType(Inf),
    nominal=ElementType(1.0),
    flow=false,
    causality=Local,
    variability=Continuous,
    integral=nothing,
    numericType=WR,
    analysis=ModiaMath.AllAnalysis) where {ValueType, ElementType} =
        RealVariable{ValueType,ElementType}(name, within, info, start, unit, fixed, min, max, nominal, false, causality, variability, numericType, integral, analysis)

function Base.show(io::IO, r::RealVariable{ValueType,ElementType}) where {ValueType, ElementType}
    print(r.value, " ", r.unit)
    if r.info != ""
      # print(" # ", r.info)
   end
end

#------------------------------ Concrete Real Variable types ----------------------------------------

"""
    v = RealScalar(..)

Generate a variable of type [`RealVariable`](@ref)`{Float64,Float64}` to define scalar, Float64, real variables.
The argument list is described in module [`Variables`](@ref).
"""
const RealScalar = RealVariable{Float64,Float64}

"""
    v = RealSVector{Size}(..)

Generate a variable of type [`RealVariable`](@ref)`{SVector{Size,Float64},Float64}` where the values have
type StaticArrays.SVector{Size,Float64}.
The argument list is described in module [`Variables`](@ref).
"""
const RealSVector{Size} = RealVariable{SVector{Size,Float64},Float64}

"""
    v = RealSVector3(..)

Generate a variable of type [`RealVariable`](@ref)`{SVector{3,Float64},Float64}` where the values have
type StaticArrays.SVector{3,Float64}.
The argument list is described in module [`Variables`](@ref).
"""
const RealSVector3 = RealSVector{3}

RealScalar(name=NoNameDefined,
    within=nothing;
    info="",
    start=0.0,
    unit=NoUnit,
    fixed=false,
    min=-Inf,
    max=Inf,
    nominal=1.0,
    flow=false,
    causality=Local,
    variability=Continuous,
    integral=nothing,
    numericType=WR,
    analysis=ModiaMath.AllAnalysis) =
         RealScalar(name, within, info, start, unit, fixed, min, max, nominal, flow, causality, variability, numericType, integral, analysis)

RealSVector{Size}(name=NoNameDefined,
    within=nothing;
    info="",
    start=zeros(Size),
    unit=NoUnit,
    fixed=false,
    min=-Inf,
    max=Inf,
    nominal=1.0,
    flow=false,
    causality=Local,
    variability=Continuous,
    integral=nothing,
    numericType=WR,
    analysis=ModiaMath.AllAnalysis) where {Size} =
        RealSVector{Size}(name, within, info, start, unit, fixed, min, max, nominal, flow, causality, variability, numericType, integral, analysis)


#=
Copyright (c) 2015, Intel Corporation

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice,
      this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of Intel Corporation nor the names of its contributors
      may be used to endorse or promote products derived from this software
      without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
=#

export TransformLayer
using HDF5

type TransformEnsemble <: JuliaEnsemble
    net           :: Net
    name          :: Symbol
    neurons       :: Array{DataNeuron}
    mean          :: Array{Float32}
    random_mirror :: Bool
    scale         :: Float32
    connections   :: Vector{Connection}
    phase         :: Phase
    net_subgroup  :: Cint
end

function TransformLayer(net::Net, name::Symbol,
                        input_ensemble::AbstractEnsemble; crop=false,
                        random_mirror=false, scale=1.0f0, mean_file=nothing)
    @assert ndims(input_ensemble) == 3
    if crop != false
        neurons = Array(DataNeuron, crop..., size(input_ensemble, 3))
    else
        neurons = Array(DataNeuron, size(input_ensemble)...)
    end
    mean = zeros(Float32, size(input_ensemble)...)
    if mean_file != nothing
        h5open(mean_file, "r") do h5
            mean = h5["mean"]
            if ndims(mean) == 4
                mean = mean[:,:,:,1] .* scale
                mean = reshape(mean, size(mean)[1:3])
            else
                mean = mean[:,:,:] .* scale
            end
            @assert size(mean) == size(input_ensemble)
        end
    end
    for i = 1:length(neurons)
        neurons[i] = DataNeuron(0.0f0)
    end
    ens = TransformEnsemble(net, name, neurons, mean, random_mirror, scale, [], TrainTest, convert(Cint, 1))
    add_ensemble(net, ens)
    add_connections(net, input_ensemble, ens, (i, j, k) -> (i:i, j:j, k:k))
    ens
end

function forward(ens::TransformEnsemble, input::Array, value::Array, phase::Phase)
    if phase == Train
        w_off = abs(rand(Int)) % (size(input,1)-size(value, 1)+1)
        h_off = abs(rand(Int)) % (size(input,2)-size(value, 2)+1)
    else
        w_off = div(size(input,1)-size(input, 1), 2)
        h_off = div(size(input,2)-size(input, 2), 2)
    end
    for i in 1:size(input, 4)
        input[:,:,:,i] -= ens.mean
    end
    crop_w, crop_h, channels, num = size(value)
    if rand(UInt) % 2 == 0 || phase == Test
        for n = 1:num
            for c = 1:channels
                for h = 1:crop_h
                    @simd for w = 1:crop_w
                        @inbounds value[w,h,c,n] = input[w+w_off,h+h_off,c,n] * ens.scale
                    end
                end
            end
        end
    else
        for n = 1:num
            for c = 1:channels
                for h = 1:crop_h
                    @simd for w = 1:crop_w
                        @inbounds value[crop_w-w+1,h,c,n] = input[w+w_off,h+h_off,c,n] * ens.scale
                    end
                end
            end
        end
    end
end

function backward(ens::TransformEnsemble, args...)
end


using BenchmarkTools
using CUDA
using DataDeps
using Oceananigans
using Benchmarks

# Need a grid

ENV["DATADEPS_ALWAYS_ACCEPT"] = "true"

dd = DataDep("cubed_sphere_510_grid",
    "Conformal cubed sphere grid with 510×510 grid points on each face",
    "https://engaging-web.mit.edu/~alir/cubed_sphere_grids/cs510/cubed_sphere_510_grid.jld2"
)

DataDeps.register(dd)

# Benchmark function

# All grids have 6 * 510^2 = 1,560,600 grid points.
grids = Dict(
     (CPU, :RegularRectilinearGrid)       => RegularRectilinearGrid(size=(1445, 1080, 1), extent=(1, 1, 1)),
     (CPU, :RegularLatitudeLongitudeGrid) => RegularLatitudeLongitudeGrid(size=(1445, 1080, 1), longitude=(-180, 180), latitude=(-80, 80), z=(-1, 0)),
     (CPU, :ConformalCubedSphereFaceGrid) => ConformalCubedSphereFaceGrid(size=(1445, 1080, 1), z=(-1, 0)),
     (CPU, :ConformalCubedSphereGrid)     => ConformalCubedSphereGrid(datadep"cubed_sphere_510_grid/cubed_sphere_510_grid.jld2", Nz=1, z=(-1, 0)),
     (GPU, :RegularRectilinearGrid)       => RegularRectilinearGrid(size=(1445, 1080, 1), extent=(1, 1, 1)),
     (GPU, :RegularLatitudeLongitudeGrid) => RegularLatitudeLongitudeGrid(size=(1445, 1080, 1), longitude=(-180, 180), latitude=(-80, 80), z=(-1, 0)),
     # Uncomment when ConformalCubedSphereFaceGrids of any size can be built natively without loading from file:
     # (GPU, :ConformalCubedSphereFaceGrid) => ConformalCubedSphereFaceGrid(size=(1445, 1080, 1), z=(-1, 0), architecture=GPU()),
     (GPU, :ConformalCubedSphereGrid)     => ConformalCubedSphereGrid(datadep"cubed_sphere_510_grid/cubed_sphere_510_grid.jld2", Nz=1, z=(-1, 0), architecture=GPU()),
)

free_surfaces = Dict(
    :ExplicitFreeSurface => ExplicitFreeSurface(),
    :ImplicitFreeSurface => ImplicitFreeSurface(maximum_iterations=1, tolerance=-Inf) # Force it to take exactly 1 iteration.
)

function benchmark_hydrostatic_model(Arch, grid_type, free_surface_type)

    model = HydrostaticFreeSurfaceModel(
              architecture = Arch(),
                      grid = grids[(Arch, grid_type)],
        momentum_advection = VectorInvariant(),
              free_surface = free_surfaces[free_surface_type]
    )

    time_step!(model, 1) # warmup

    trial = @benchmark begin
        CUDA.@sync blocking=true time_step!($model, 1)
    end samples=10

    return trial
end

# Benchmark parameters

Architectures = has_cuda() ? [CPU, GPU] : [CPU]

grid_types = [
    :RegularRectilinearGrid,
    :RegularLatitudeLongitudeGrid,
    # Uncomment when ConformalCubedSphereFaceGrids of any size can be built natively without loading from file:
    # :ConformalCubedSphereFaceGrid,
    :ConformalCubedSphereGrid
]

free_surface_types = [
    :ExplicitFreeSurface,
    # ImplicitFreeSurface doesn't yet work on MultiRegionGrids like the ConformalCubedSphereGrid:
    # :ImplicitFreeSurface
]

# Run and summarize benchmarks

print_system_info()
suite = run_benchmarks(benchmark_hydrostatic_model; Architectures, grid_types, free_surface_types)

df = benchmarks_dataframe(suite)
# sort!(df, [:Architectures, :Float_types, :Ns], by=(string, string, identity))
benchmarks_pretty_table(df, title="Hydrostatic model benchmarks")


import NonUniformRandomVariateGeneration.sampleCategorical

@inline function sampleOne(xs::Vector{Float64}, sumXs::Float64, rpio::RPIO)
  M::Int64 = length(xs)
  if rpio.threads > 1 && M > PARALLEL_COUNTS_THRESHOLD
    # rpio.scratch contains the summed weights for each block
    idx::Int64 = sampleOne(rpio.scratch, sumXs)
    nt::Int64 = rpio.threads
    b::Int64 = div(M, nt)
    start::Int64 = (idx-1)*b + 1
    @inbounds return sampleOne(xs, rpio.scratch[idx], start)
  else
    return sampleCategorical(xs, sumXs)
  end
end

@inline function rpEstimate(model::Model, rpio::RPIO{Particle}) where Particle
  potentials::Vector{Float64} = rpio.potentials
  ks::Vector{Int64} = rpio.ks
  v::Float64 = 0.0
  _sampleParticles(model, rpio)
  b::Int64 = div(rpio.N, rpio.threads)
  for p in 1:rpio.n
    s::Float64 = _evaluatePotentials(model, rpio, p)
    for j in 1:p-1
      @inbounds block::Int64 = min(ceil(ks[j]/b), rpio.threads)
      @inbounds rpio.scratch[block] -= potentials[ks[j]]
      @inbounds s -= potentials[ks[j]]
      @inbounds potentials[ks[j]] = 0.0
    end
    v += log(s / (rpio.N-p+1))
    if p < model.n
      @inbounds ks[p] = sampleOne(potentials, s, rpio)
    end
  end
  return v
end

@inline function rpEstimateDiscrete(model::Model, rpio::RPIO{Int64Particle})
  _sampleParticles(model, rpio)
  ζcounts::Vector{Int64} = _getCounts(rpio)
  potentials::Vector{Float64} = Vector{Float64}(undef, length(ζcounts))

  v::Float64 = 0.0
  for p in 1:rpio.n
    s::Float64 = _countPotentials(potentials, ζcounts, model, rpio, p)
    v += log(s / (rpio.N-p+1))
    if p < model.n
      k::Int64 = sampleOne(potentials, s, rpio)
      @inbounds ζcounts[k] -= 1
    end
  end
  return v
end


module Orbitals

import Base.getindex
import Base.lastindex
import Base.setindex!
import Base.length

abstract type AbstractOrbital end

struct CanonicalOrbital <: AbstractOrbital
    C::Array{Float64,1}
end # module
struct NaturalOrbital <: AbstractOrbital
    C::Array{Float64,1}
    ω::Float64 # occupation number
end
struct LocalOrbital <: AbstractOrbital
    C::Array{Float64,1}
end
struct BruecknerOrbital <: AbstractOrbital
    C::Array{Float64,1}
end

abstract type AbstractOrbitals end

struct CanonicalOrbitals <: AbstractOrbitals
    orbs::Array{CanonicalOrbital}
end
struct NaturalOrbitals <: AbstractOrbitals
    orbs::Array{NaturalOrbitals}
end
struct LocalOrbitals <: AbstractOrbitals
    orbs::Array{LocalOrbitals}
end
struct BruecknerOrbitals <: AbstractOrbitals
    orbs::Array{BruecknerOrbital}
end

function CanonicalOrbitals(C::Array{Float64,2})
    return CanonicalOrbitals([CanonicalOrbital(Array{Float64,1}(C[:,i])) for i in 1:size(C,2)])
end

function toarray(orbs::O) where O <: AbstractOrbitals
    C = hcat([orb.C for orb in orbs.orbs]...)
end

function Base.getindex(orb::O,i) where O <: AbstractOrbitals
    return orb.orbs[i]
end
function Base.getindex(orb::O,R::UnitRange) where O <: AbstractOrbitals
    return O([orb.orbs[i] for i in R])
end

function Base.lastindex(orb::O) where O <: AbstractOrbitals
    return length(orb.orbs)
end

function Base.length(orb::O) where O <: AbstractOrbitals
    return length(orb.orbs)
end

mutable struct OrbDict
    current::String
    ndocc::Int
    nvir::Int
    frozencore::Int
    frozenvir::Int
    cache::Dict{String,O} where O <: AbstractOrbitals
end

function matchphase(orbs,oldC)
    for oi in eachindex(orbs.cache[orbs.current].orbs)
        o = orbs.cache[orbs.current].orbs[oi]
        osorted = reverse(sort(o.C,by=abs))
        csorted = reverse(sort(oldC[:,oi],by=abs))
        if sign(osorted[1]) != sign(csorted[1])
            o.C .*= -1
        end
    end
end
"""
call like readC(ints.orbs,Cmatrix)
"""
function readC(orbs,newC)
    for oi in eachindex(orbs.cache[orbs.current].orbs)
        o = orbs.cache[orbs.current].orbs[oi]
        o.C .= newC[:,oi]
    end
end
function topositive!(orbs::OrbDict)
    for o in orbs.cache[orbs.current].orbs
        sorted = reverse(sort(o.C,by=abs))
        if sorted[1] >= 0
            o.C .*= 1
        else
            o.C .*= -1
        end
    end
end
function rotate!(orbs::OrbDict,U::Array{Float64,2};fc=0,fv=0) where O <: AbstractOrbitals
    C = hcat([orb.C for orb in orbs["OV"].orbs]...)
    C = C*U
    for i=1:length(orbs["OV"])
        _i = i+fc
        orbs.cache[orbs.current].orbs[_i].C[:] .= C[:,i]
    end
end

function OrbDict()
    OrbDict("canonical",0,0,0,0,Dict{String,AbstractOrbitals}())
end

function activate!(OD::OrbDict,entry::String)
    if !(entry in keys(OD.cache))
        error("OrbDict tried to activate a non-existent basis!")
    else
        OD.current = entry
    end
end

function Base.getindex(OD::OrbDict,entry::String)
    gather = occursin("[",entry)
    s = strip(entry,['[',']'])
    all = OD.cache[OD.current]
    type = typeof(all)
    fc = OD.frozencore
    fv = OD.frozenvir
    lookup = Dict{Char,type}(
                             'F' => all[1:OD.ndocc],
                             'O' => all[fc+1:OD.ndocc],
                             'U' => all[OD.ndocc+1:OD.ndocc+OD.nvir],
                             'V' => all[OD.ndocc+1:OD.ndocc+OD.nvir-fv]
                            )
    out = []
    for i in eachindex(s)
        _s = lookup[s[i]]
        if length(_s) != 0
            push!(out,lookup[s[i]])
        end
    end
    out = type(hcat([toarray(o) for o in out]...))
    if gather
        return toarray(out)
    else
        return out
    end
end

function Base.setindex!(OD::OrbDict,val,entry::String)
    OD.cache[entry] = val
end

end #module


using AlignedSpans
using AlignedSpans: n_samples, indices
using Test
using TimeSpans, Dates, Onda, Intervals
using JSON3, Arrow
using Aqua

function make_test_samples(sample_rate)
    return Samples(permutedims([0:100 10:110]),
                   SamplesInfo(; kind="test", channels=["a", "b"], sample_unit="microvolt",
                               sample_resolution_in_unit=0.5, sample_offset_in_unit=0.0,
                               sample_type=UInt16,
                               sample_rate), false)
end

# Interop with `StepRange`
function Base.StepRange(span::AlignedSpan)
    # the rounding here is not ideal
    t = Nanosecond(round(Int, TimeSpans.nanoseconds_per_sample(span.sample_rate)))
    return (span.i * t):t:(span.j * t)
end

function AlignedSpan(r::StepRange{T,S}) where {T<:Period,S<:Period}
    sample_rate = TimeSpans.NS_IN_SEC / Dates.value(convert(Nanosecond, step(r)))
    i = first(r) / step(r)
    j = last(r) / step(r)
    return AlignedSpan(sample_rate, Int(i), Int(j))
end

@testset "AlignedSpans.jl" begin
    @testset "Aqua" begin
        Aqua.test_all(AlignedSpans; ambiguities=false)
    end

    @testset "RoundInward" begin
        for span in (TimeSpan(Millisecond(1500), Millisecond(2500)),
                     TimeSpan(Millisecond(1001), Millisecond(2001)),
                     TimeSpan(Millisecond(1001), Millisecond(2999)),
                     TimeSpan(Millisecond(2000), Millisecond(2001)))
            # all spans include only sample 3, at second 2

            aligned = AlignedSpan(1, span, RoundInward)
            # Only sample included inside `span` is sample 3
            @test indices(aligned) == 3:3

            @test TimeSpan(aligned) == TimeSpan(Second(2), Second(3))
        end

        # Does *not* contain any samples
        span = TimeSpan(Millisecond(1999), Millisecond(2000))
        @test_throws ArgumentError("No samples lie within `span`") AlignedSpan(1, span,
                                                                               RoundInward)
    end

    @testset "RoundSpanDown" begin
        for span in (TimeSpan(Millisecond(1500), Millisecond(2500)),
                     TimeSpan(Millisecond(1001), Millisecond(2001)),
                     TimeSpan(Millisecond(1001), Millisecond(2999)))
            aligned = AlignedSpan(1, span, RoundSpanDown)
            # Only sample included inside `span` is sample 3, but we round left endpoint down
            @test indices(aligned) == 2:3

            @test TimeSpan(aligned) == TimeSpan(Second(1), Second(3))
        end

        span = TimeSpan(Millisecond(2000), Millisecond(2001))
        aligned = AlignedSpan(1, span, RoundSpanDown)
        @test indices(aligned) == 3:3

        span = TimeSpan(Millisecond(1999), Millisecond(2000))
        aligned = AlignedSpan(1, span, RoundSpanDown)
        @test indices(aligned) == 2:2
    end

    @testset "ConstantSamplesRoundingMode" begin
        mode = ConstantSamplesRoundingMode(RoundDown)

        span = TimeSpan(Millisecond(1500), Millisecond(2500))
        aligned = AlignedSpan(1, span, mode)
        inds = indices(aligned)
        @test length(inds) == n_samples(1, duration(span))
        @test inds == 2:2
        for t in 1:100
            translated = translate(span, Second(t))
            aligned = AlignedSpan(1, translated, mode)
            inds = indices(aligned)
            @test length(inds) == n_samples(aligned) == n_samples(1, duration(span))
        end

        for span in (TimeSpan(Millisecond(1500), Millisecond(2600)),
                     Interval{Nanosecond,Closed,Open}(Millisecond(1500), Millisecond(2600)))
            aligned = AlignedSpan(1, span, mode)
            @test indices(aligned) == 2:2
        end

        # We should get the same number of samples no matter how we translate it
        for t in [Millisecond(1):Millisecond(1):Millisecond(1000);
                  Nanosecond(10):Nanosecond(10):Nanosecond(1000)]
            translated_span = TimeSpans.translate(span, t)
            aligned = AlignedSpan(1, translated_span, mode)
            @test length(indices(aligned)) ==
                  n_samples(aligned) ==
                  length(2:2)
        end
    end

    include("interop.jl")
    include("time_index_conversions.jl")
    include("utilities.jl")
end


@pywrap struct IB
end

IB() = IB(ib_insync.IB())
Base.isopen(ib::IB) = ib.isConnected()
function Base.open(ib::IB, args...; kwargs...)
    ib.connect(args...; kwargs...)
    return ib
end
function Base.close(ib::IB)
    isopen(ib) && ib.disconnect()
    return nothing
end

####
#### IBAccount
####

# TODO currency field?
# TODO portfolio margin doesn't have SMA? ELV?
struct IBAccount{C<:CashQuantity} <: Account
    "Account name or number"
    name::String

    "ELV – Initial Margin. Liquidation occurs if < 0 at any time."
    excess_liquidity::C
    "Maximum of (Prior Day SMA +/- Change in Day's Cash +/- Today's Trades Initial Margin Requirements) and
    (ELV - Reg T Margin). Liquidation occurs if < 0 at end of trading day (1550 ET)."
    sma::C
    "Funds available for trading. Equal to ELV – Initial Margin."
    available_funds::C

    "GPV: Absolute value of Long Stock Value + Short Stock Value + Long Option Value + Short Option Value + Long SSF Notional Value + Short SSF Notional Value + Fund Value."
    gross_position_value::C
    "ELV: Equals cash + stock value + bond value + mutual fund value + European and Asian options value (excludes market value U.S. securities & futures options and cash maintained in futures segment)."
    equity_with_loan_value::C
    "NLV: For a securities account equals total cash value + stock value + securities options value + bond value + fund value."
    net_liquidation_value::C
    "Total cash balance. Negative value represented borrowed money."
    total_cash_value::C
end

function IBAccount(ib::IB, account::String)
    sum = ib.accountSummary(account)
    function get(fname, tag)
        idx = findfirst(x -> x[2] == tag, sum)
        idx == nothing && error("Unknown tag $tag")
        _, tag, value, currency, model_code = sum[idx]
        T = fieldtype(IBAccount, fname)
        if T <: CashQuantity
            @assert currency != ""
            value = parse(Float64, value) * parse_currency(currency)
        elseif T <: Number
            value = parse(T, value)
        end
        return value
    end
    IBAccount(
        account,
        get(:excess_liquidity, "ExcessLiquidity"),
        get(:sma, "SMA"),
        get(:available_funds, "AvailableFunds"),
        get(:gross_position_value, "GrossPositionValue"),
        get(:equity_with_loan_value, "EquityWithLoanValue"),
        get(:net_liquidation_value, "NetLiquidation"),
        get(:total_cash_value, "TotalCashValue")
    )
end

accounts(ib::IB) = map(n -> IBAccount(ib, n), managed_accounts(ib))

managed_accounts(ib::IB) = ib.managedAccounts()

function positions(ib::IB, account::IBAccount)
    ps = Dict{String,Position}()
    for (c, position, mkt_price, mkt_value, avg_cost, _, _, _account) in ib.portfolio()
        account.name == _account || continue

        Tname = pyname(c)
        currency = parse_currency(c.currency)
        c = Tname == "Stock" ? Stock(c.symbol, c.exchange, currency) :
            error("Unknown position type: $Tname")
        sym = fullsymbol(c)

        @assert !haskey(ps, sym)
        ps[sym] = Position(c, position, mkt_price * currency, mkt_value * currency, avg_cost * currency)
    end
    return ps
end

####
#### Conversion
####

function PyCall.PyObject(x::Stock)
    ib_insync.Stock(x.symbol, x.exchange, string(x.currency))
end

# function PyCall.PyObject(x::Stock)
#     ib_insync.Stock(x.symbol, x.exchange, string(x.currency))
# end
#
# Base.convert(::Type{<:Stock}, o::PyObject) = Stock(o.symbol, o.exchange, parse_currency(o.currency))


struct FaceToCellGlue{O} <: GridapType
  orientation::Val{O}
  face_to_cell::Vector{Int}
  face_to_lface::Vector{Int8}
  cell_to_ctype::Vector{Int8}
  cell_to_lface_to_pindex::Table{Int8,Vector{Int8},Vector{Int32}}
  ctype_to_lface_to_ftype::Vector{Vector{Int8}}
end

is_oriented(::FaceToCellGlue{O}) where O = O

"""
    struct GenericBoundaryTriangulation{Dc,Dp,Gf,Gc,O} <: BoundaryTriangulation{Dc,Dp}
      face_trian::Gf
      cell_trian::Gc
      # + private fields
    end
"""
struct GenericBoundaryTriangulation{Dc,Dp,Gf,Gc,O,A} <: BoundaryTriangulation{Dc,Dp}
  face_trian::Gf
  cell_trian::Gc
  glue::FaceToCellGlue{O}
  cell_around::A

  function GenericBoundaryTriangulation(
    face_trian::Triangulation,
    cell_trian::Triangulation,
    glue::FaceToCellGlue{O},
    cell_around=1) where O

    Dc = num_cell_dims(face_trian)
    Dp = num_point_dims(face_trian)
    Gf = typeof(face_trian)
    Gc = typeof(cell_trian)
    A = typeof(cell_around)
    new{Dc,Dp,Gf,Gc,O,A}(face_trian, cell_trian, glue, cell_around)
  end

end

function GenericBoundaryTriangulation(
  face_trian::Triangulation,
  cell_trian::Triangulation,
  topo::GridTopology,
  face_to_oldface::Vector{Int},
  icell_around=1)

  D = num_cell_dims(cell_trian)
  oldface_to_cells = get_faces(topo,D-1,D)
  cell_to_oldfaces = get_faces(topo,D,D-1)
  cell_to_lface_to_pindex = Table(get_cell_permutations(topo,D-1))
  orientation = OrientationStyle(topo)

  oldface_to_cell = get_local_item(oldface_to_cells, icell_around)
  oldface_to_lface = find_local_index(oldface_to_cell, cell_to_oldfaces)
  face_to_cell = collect(Int,reindex(oldface_to_cell, face_to_oldface))
  face_to_lface = collect(Int8,reindex(oldface_to_lface, face_to_oldface))

  f = (p)->fill(Int8(UNSET),num_faces(p,D-1))
  ctype_to_lface_to_ftype = map( f, get_reffes(cell_trian) )
  face_to_ftype = get_cell_type(face_trian)
  cell_to_ctype = collect(Int8,get_cell_type(cell_trian))
  _fill_ctype_to_lface_to_ftype!(
    ctype_to_lface_to_ftype,
    face_to_cell,
    face_to_lface,
    face_to_ftype,
    cell_to_ctype)

  glue = FaceToCellGlue(
    orientation,
    face_to_cell,
    face_to_lface,
    cell_to_ctype,
    cell_to_lface_to_pindex,
    ctype_to_lface_to_ftype)

  GenericBoundaryTriangulation(face_trian, cell_trian, glue, icell_around)

end

"""
    GenericBoundaryTriangulation(model::DiscreteModel,face_to_mask::Vector{Bool})
"""
function GenericBoundaryTriangulation(
  model::DiscreteModel,face_to_mask::Vector{Bool},icell_around=1)
  D = num_cell_dims(model)
  topo = get_grid_topology(model)
  oldface_grid = Grid(ReferenceFE{D-1},model)
  cell_grid = Grid(ReferenceFE{D},model)
  face_to_oldface = findall(face_to_mask)
  face_trian = TriangulationPortion(oldface_grid,face_to_oldface)
  GenericBoundaryTriangulation(face_trian,cell_grid,topo,face_to_oldface,icell_around)
end

function _fill_ctype_to_lface_to_ftype!(
  ctype_to_lface_to_ftype,
  face_to_cell,
  face_to_lface,
  face_to_ftype,
  cell_to_ctype)
  for (face, cell) in enumerate(face_to_cell)
    ctype = cell_to_ctype[cell]
    lface = face_to_lface[face]
    ftype = face_to_ftype[face]
    ctype_to_lface_to_ftype[ctype][lface] = ftype
  end
end

# Triangulation Interface

function get_cell_coordinates(trian::GenericBoundaryTriangulation)
  get_cell_coordinates(trian.face_trian)
end

function get_reffes(trian::GenericBoundaryTriangulation)
    get_reffes(trian.face_trian)
end

function get_cell_type(trian::GenericBoundaryTriangulation)
  get_cell_type(trian.face_trian)
end

function get_face_to_cell(trian::GenericBoundaryTriangulation)
  trian.glue.face_to_cell
end

function get_face_to_lface(trian::GenericBoundaryTriangulation)
  trian.glue.face_to_lface
end

function get_volume_triangulation(trian::GenericBoundaryTriangulation)
  trian.cell_trian
end

function get_face_to_face(trian::GenericBoundaryTriangulation)
  trian.face_trian.cell_to_oldcell
end

function get_cell_around(trian::GenericBoundaryTriangulation)
  trian.cell_around
end

# Cell to face map

function get_face_to_cell_map(trian::GenericBoundaryTriangulation)
  face_to_fvertex_to_qcoors = FaceCellCoordinates(trian)
  f = (p)-> get_shapefuns(LagrangianRefFE(Float64,get_polytope(p),1))
  ftype_to_shapefuns = map( f, get_reffes(trian.face_trian) )
  face_to_ftype = collect(Int8, get_cell_type(trian.face_trian))
  face_to_shapefuns = CompressedArray(ftype_to_shapefuns, face_to_ftype)
  lincomb(face_to_shapefuns,face_to_fvertex_to_qcoors)
end

struct FaceCellCoordinates{D,T,O} <: AbstractVector{Vector{Point{D,T}}}
  glue::FaceToCellGlue{O}
  cell_to_ctype::Vector{Int8}
  ctype_to_lvertex_to_qcoords::Vector{Vector{Point{D,T}}}
  ctype_to_lface_to_lvertices::Vector{Vector{Vector{Int}}}
  ctype_to_lface_to_pindex_to_perm::Vector{Vector{Vector{Vector{Int}}}}
  ctype_to_lface_to_pindex_to_qcoords::Vector{Vector{Vector{Vector{Point{D,T}}}}}
  function FaceCellCoordinates(trian::GenericBoundaryTriangulation)

    d = num_cell_dims(trian)
    polytopes = map(get_polytope, get_reffes(trian.cell_trian))
    cell_to_ctype = trian.glue.cell_to_ctype
    ctype_to_lvertex_to_qcoords = map(get_vertex_coordinates, polytopes)
    ctype_to_lface_to_lvertices = map((p)->get_faces(p,d,0), polytopes)
    ctype_to_lface_to_pindex_to_perm = map( (p)->get_face_vertex_permutations(p,d), polytopes)

    P = eltype(eltype(ctype_to_lvertex_to_qcoords))
    D = length(P)
    T = eltype(P)
    O = is_oriented(trian.glue)

    ctype_to_lface_to_pindex_to_qcoords = Vector{Vector{Vector{Point{D,T}}}}[]
    for (ctype, lface_to_pindex_to_perm) in enumerate(ctype_to_lface_to_pindex_to_perm)
      lvertex_to_qcoods = ctype_to_lvertex_to_qcoords[ctype]
      lface_to_pindex_to_qcoords = Vector{Vector{Point{D,T}}}[]
      for (lface, pindex_to_perm) in enumerate(lface_to_pindex_to_perm)
        cfvertex_to_lvertex = ctype_to_lface_to_lvertices[ctype][lface]
        nfvertices = length(cfvertex_to_lvertex)
        pindex_to_qcoords = Vector{Vector{Point{D,T}}}(undef,length(pindex_to_perm))
        for (pindex, cfvertex_to_ffvertex) in enumerate(pindex_to_perm)
          ffvertex_to_qcoords = zeros(Point{D,T},nfvertices)
          for (cfvertex, ffvertex) in enumerate(cfvertex_to_ffvertex)
            lvertex = cfvertex_to_lvertex[cfvertex]
            qcoords = lvertex_to_qcoods[lvertex]
            ffvertex_to_qcoords[ffvertex] = qcoords
          end
          pindex_to_qcoords[pindex] = ffvertex_to_qcoords
        end
        push!(lface_to_pindex_to_qcoords,pindex_to_qcoords)
      end
      push!(ctype_to_lface_to_pindex_to_qcoords,lface_to_pindex_to_qcoords)
    end

    new{D,T,O}(
      trian.glue,
      cell_to_ctype,
      ctype_to_lvertex_to_qcoords,
      ctype_to_lface_to_lvertices,
      ctype_to_lface_to_pindex_to_perm,
      ctype_to_lface_to_pindex_to_qcoords)

  end
end

Base.IndexStyle(::Type{FaceCellCoordinates{D,T,O}}) where {D,T,O} = IndexLinear()

Base.size(a::FaceCellCoordinates) = (length(a.glue.face_to_cell),)

function Base.getindex(a::FaceCellCoordinates,face::Integer)
  cell = a.glue.face_to_cell[face]
  lface = a.glue.face_to_lface[face]
  ctype = a.cell_to_ctype[cell]
  p = a.glue.cell_to_lface_to_pindex.ptrs[cell]-1
  pindex = a.glue.cell_to_lface_to_pindex.data[p+lface]
  a.ctype_to_lface_to_pindex_to_qcoords[ctype][lface][pindex]
end

# Value of face to cell map

function apply_lincomb(ax::CompressedArray,b::FaceCellCoordinates)
  FaceToCellMapValue(ax,b)
end

struct FaceToCellMapValue{D,T,O} <: AbstractVector{Vector{Point{D,T}}}
  b::FaceCellCoordinates{D,T,O}
  ctype_to_lface_to_pindex_to_qpoints::Vector{Vector{Vector{Vector{Point{D,T}}}}}

  function FaceToCellMapValue(ax::CompressedArray,b::FaceCellCoordinates{D,T,O}) where {D,T,O}

    # Precompute the integration point coordinates for all possible cases

    ctype_to_lface_to_ftype = b.glue.ctype_to_lface_to_ftype
    ctype_to_lface_to_lvertices = b.ctype_to_lface_to_lvertices
    ctype_to_lvertex_to_qcoords = b.ctype_to_lvertex_to_qcoords
    ctype_to_lface_to_pindex_to_perm = b.ctype_to_lface_to_pindex_to_perm
    ftype_to_shapefunsvals = ax.values

    # Allocate
    ctype_to_lface_to_pindex_to_qpoints = Vector{Vector{Vector{Point{D,T}}}}[]
    for (ctype, lface_to_pindex_to_perm) in enumerate(ctype_to_lface_to_pindex_to_perm)
      lface_to_pindex_to_qpoints = Vector{Vector{Point{D,T}}}[]
      for (lface, pindex_to_perm) in enumerate(lface_to_pindex_to_perm)
        pindex_to_qpoints = Vector{Vector{Point{D,T}}}(undef,length(pindex_to_perm))
        push!(lface_to_pindex_to_qpoints,pindex_to_qpoints)
      end
      push!(ctype_to_lface_to_pindex_to_qpoints,lface_to_pindex_to_qpoints)
    end

    # Fill
    for (ctype, lface_to_ftype) in enumerate(ctype_to_lface_to_ftype)
      for (lface, ftype) in enumerate(lface_to_ftype)
        if ftype == UNSET
          continue
        end

        cfvertex_to_lvertex = ctype_to_lface_to_lvertices[ctype][lface]
        pindex_to_perm = ctype_to_lface_to_pindex_to_perm[ctype][lface]
        lvertex_to_qcoods = ctype_to_lvertex_to_qcoords[ctype]
        nfvertices = length(cfvertex_to_lvertex)
        ffvertex_to_qcoords = zeros(Point{D,T},nfvertices)
        shapefunsvals = ftype_to_shapefunsvals[ftype]

        for (pindex, cfvertex_to_ffvertex) in enumerate(pindex_to_perm)
          for (cfvertex, ffvertex) in enumerate(cfvertex_to_ffvertex)
            lvertex = cfvertex_to_lvertex[cfvertex]
            qcoords = lvertex_to_qcoods[lvertex]
            ffvertex_to_qcoords[ffvertex] = qcoords
          end
          qpoints = shapefunsvals*ffvertex_to_qcoords
          ctype_to_lface_to_pindex_to_qpoints[ctype][lface][pindex] = qpoints
        end
      end
    end

    new{D,T,O}(b,ctype_to_lface_to_pindex_to_qpoints)

  end
end

Base.size(v::FaceToCellMapValue) = (length(v.b.glue.face_to_cell),)

Base.IndexStyle(::Type{FaceToCellMapValue{C,T,O}}) where {C,T,O} = IndexLinear()

function Base.getindex(a::FaceToCellMapValue,face::Integer)
  cell = a.b.glue.face_to_cell[face]
  lface = a.b.glue.face_to_lface[face]
  ctype = a.b.cell_to_ctype[cell]
  p = a.b.glue.cell_to_lface_to_pindex.ptrs[cell]-1
  pindex = a.b.glue.cell_to_lface_to_pindex.data[p+lface]
  a.ctype_to_lface_to_pindex_to_qpoints[ctype][lface][pindex]
end

function evaluate_field_array(g::CompressedArray{<:Field},fx::FaceToCellMapValue)
  CellShapeFunsAtFaces(g,fx)
end

struct CellShapeFunsAtFaces{D,T,O,V} <: AbstractVector{V}
  b::FaceCellCoordinates{D,T,O}
  ctype_to_lface_to_pindex_to_shfnsvals::Vector{Vector{Vector{V}}}
  function CellShapeFunsAtFaces(g::CompressedArray{<:Field},fx::FaceToCellMapValue{D,T,O}) where {D,T,O}

    # Precompute shapefuns for all cases

    ctype_to_shapefuns = g.values
    face_to_ctype = g.ptrs
    ctype_to_lface_to_pindex_to_perm = fx.b.ctype_to_lface_to_pindex_to_perm
    ctype_to_lface_to_pindex_to_qpoints = fx.ctype_to_lface_to_pindex_to_qpoints
    ctype_to_lface_to_ftype = fx.b.glue.ctype_to_lface_to_ftype

    # Allocate
    sfuns = first(ctype_to_shapefuns)
    V = field_return_type(sfuns,zeros(Point{D,T},1))
    ctype_to_lface_to_pindex_to_shfnsvals = Vector{Vector{V}}[]
    for (ctype, lface_to_pindex_to_perm) in enumerate(ctype_to_lface_to_pindex_to_perm)
      lface_to_pindex_to_shfnsvals = Vector{V}[]
      for (lface, pindex_to_perm) in enumerate(lface_to_pindex_to_perm)
        pindex_to_shfnsvals = Vector{V}(undef,length(pindex_to_perm))
        push!(lface_to_pindex_to_shfnsvals,pindex_to_shfnsvals)
      end
      push!(ctype_to_lface_to_pindex_to_shfnsvals,lface_to_pindex_to_shfnsvals)
    end

    # Fill
    for (ctype, lface_to_ftype) in enumerate(ctype_to_lface_to_ftype)
      for (lface, ftype) in enumerate(lface_to_ftype)
        if ftype == UNSET
          continue
        end
        pindex_to_perm = ctype_to_lface_to_pindex_to_perm[ctype][lface]
        sfuns = ctype_to_shapefuns[ctype]
        for (pindex, cfvertex_to_ffvertex) in enumerate(pindex_to_perm)
          qpoints = ctype_to_lface_to_pindex_to_qpoints[ctype][lface][pindex]
          shfnsvals = evaluate(sfuns,qpoints)
          ctype_to_lface_to_pindex_to_shfnsvals[ctype][lface][pindex] = shfnsvals
        end
      end
    end

    new{D,T,O,V}(fx.b,ctype_to_lface_to_pindex_to_shfnsvals)

  end
end

Base.size(v::CellShapeFunsAtFaces) = (length(v.b.glue.face_to_cell),)

Base.IndexStyle(::Type{CellShapeFunsAtFaces{C,T,O,V}}) where {C,T,O,V} = IndexLinear()

function Base.getindex(a::CellShapeFunsAtFaces,face::Integer)
  cell = a.b.glue.face_to_cell[face]
  lface = a.b.glue.face_to_lface[face]
  ctype = a.b.cell_to_ctype[cell]
  p = a.b.glue.cell_to_lface_to_pindex.ptrs[cell]-1
  pindex = a.b.glue.cell_to_lface_to_pindex.data[p+lface]
  a.ctype_to_lface_to_pindex_to_shfnsvals[ctype][lface][pindex]
end

# Normal vector

function get_normal_vector(trian::GenericBoundaryTriangulation)
  k = NormalVectorValued()
  refn = ReferenceNormal(trian)
  cell_map = restrict(get_cell_map(trian.cell_trian),trian)
  J = gradient(cell_map)
  a = apply(k,J,refn)
  GenericCellField(a,cell_map)
end

struct ReferenceNormal{D,T} <: AbstractVector{Point{D,T}}
  face_to_cell::Vector{Int}
  face_to_lface::Vector{Int8}
  cell_to_ctype::Vector{Int8}
  ctype_to_lface_to_qnormal::Vector{Vector{Point{D,T}}}
end

function ReferenceNormal(trian::GenericBoundaryTriangulation)
  face_to_cell = trian.glue.face_to_cell
  face_to_lface = trian.glue.face_to_lface
  cell_to_ctype = trian.glue.cell_to_ctype
  f = (r) -> get_facet_normals(get_polytope(r))
  ctype_to_lface_to_qnormal = map(f, get_reffes(trian.cell_trian))
  ReferenceNormal(
    face_to_cell,
    face_to_lface,
    cell_to_ctype,
    ctype_to_lface_to_qnormal)
end

Base.size(a::ReferenceNormal) = (length(a.face_to_cell),)

Base.IndexStyle(::Type{ReferenceNormal{D,T}}) where {D,T} = IndexLinear()

function Base.getindex(a::ReferenceNormal,face::Integer)
  cell = a.face_to_cell[face]
  lface = a.face_to_lface[face]
  ctype = a.cell_to_ctype[cell]
  a.ctype_to_lface_to_qnormal[ctype][lface]
end

struct NormalField <: Field end

struct NormalVectorValued <: Kernel end

function apply_kernel!(cache,k::NormalVectorValued,J,nref)
  NormalField()
end

function kernel_evaluate(k::NormalVectorValued,x,J,refn)
  Jx = evaluate_field_array(J,x)
  k = bcast(_map_normal)
  apply(k,Jx,refn)
end

function _map_normal(J::TensorValue{D,D,T},n::VectorValue{D,T}) where {D,T}
  v = inv(J)⋅n
  m = sqrt(inner(v,v))
  if m < eps()
    return zero(n)
  else
    return v/m
  end
end


function sequencedist(x::Seq,y::Seq;sub::Int=1,ins::Int=10,del::Int=10)
	levenshtein(x[2],y[2],del,ins,sub) # high penalty for indels
end

function sequencedist(x::Reference,y::Reference;sub::Int=1,ins::Int=10,del::Int=10)
	@assert all(v->isequal(v[1][1],v[2][1]), zip(x, y)) "Cannot compare reference sequences with different names"

	# sum distance
	sum(v->sequencedist(v[1],v[2],sub=sub,ins=ins,del=del), zip(x, y))
end

# elementwise comparison of strings
function hamming(x::Seq,y::Seq)
	length(x[2])!=length(y[2]) && return typemax(Int)
	s = 0
	for (cx,cy) in zip(x[2],y[2])
		s += cx!=cy
	end
	s
end

function hamming(x::Reference,y::Reference) # not used atm.
	sum(v->hamming(v[1],v[2]), zip(x,y))
end

function hasindels(x::Seq,y::Seq)
	length(x[2]) != length(y[2]) && return true # different length, must have indels
	sequencedist(x,y) != hamming(x,y) # if levenshtein distance is different from hamming distance, there are indels
end

function hasindels(x::Reference,y::Reference)
	any(v->hasindels(v[1],v[2]), zip(x,y))
end

function reference_sanity_check(sample::Sample,
                                references::Array{Reference},
                                referenceNames::Array{String,1}, log)
	if isempty(sample.consensus)
		printiferror(log, "$(sample.name): No consensus file specified. (Alignment failed?)")
		return
	end

	c = loadfasta(sample.consensus)

	d = zeros(Int,length(references))
	for (i,r) in enumerate(references)
		# allow some name mismatching since we are normally including PhiX
		if length(c) == 1 && length(r) == 1
			d[i] = sequencedist(c[1],r[1]) # allow name mismatch
		elseif length(c) != length(r)
			d[i] = typemax(Int)
		else
			d[i] = sequencedist(c,r)
		end
	end

	i = indmin(d)
	j = findfirst(referenceNames,sample.referenceName)

	if d[j] > d[i]
		ri = splitdir(referenceNames[i])[2]
		rj = splitdir(referenceNames[j])[2]
		printifwarning(log, "$(sample.name) is closer to reference \"$ri\" than to reference \"$rj\".")
	end

	# are there any indels (when compared to the best reference)?
	hasindels(c, references[i]) && printifwarning(log, "$(sample.name) has indels.")
	return
end

function reference_sanity_check(samples::Vector{Sample},
                                refs::Vector;
                                log=devnull)
	r = Reference[loadfasta(f[4]) for f in refs] # loaded fastas
	names = String[f[2] for f in refs]

	for i=1:length(samples)
		reference_sanity_check(samples[i], r, names, log)
	end
end


#=
This file is auto-generated. Do not edit.
=#
"""
    mutable struct SCRX <: AVR
        Ta_Tb::Float64
        Tb::Float64
        K::Float64
        Te::Float64
        Efd_lim::NamedTuple{(:min, :max), Tuple{Float64, Float64}}
        switch::Int
        rc_rfd::Float64
        V_ref::Float64
        ext::Dict{String, Any}
        states::Vector{Symbol}
        n_states::Int
        states_types::Vector{StateTypes}
        internal::InfrastructureSystemsInternal
    end

This exciter is based on an IEEE type SCRX solid state exciter.  The output field voltage is varied by a control system to maintain the system voltage at Vref.  Please note that this exciter model has no initialization capabilities - this means that it will respond to whatever inputs it receives regardless of the state of the machine model.

# Arguments
- `Ta_Tb::Float64`: Lead input constant ratio, validation range: `(0.05, 0.3)`, action if invalid: `error`
- `Tb::Float64`: Lag input constant in s, validation range: `(5, 20)`, action if invalid: `error`
- `K::Float64`: Regulator Gain, validation range: `(20, 100)`, action if invalid: `warn`
- `Te::Float64`: Regulator Time Constant, validation range: `(0, 1)`, action if invalid: `warn`
- `Efd_lim::NamedTuple{(:min, :max), Tuple{Float64, Float64}}`: Field Voltage regulator limits (regulator output) (Efd_min, Efd_max)
- `switch::Int`: Switch, validation range: `(0, 1)`, action if invalid: `error`
- `rc_rfd::Float64`: Field current capability. Set = 0 for negative current capability. Typical value 10, validation range: `(0, 10)`, action if invalid: `warn`
- `V_ref::Float64`: Reference Voltage Set-point, validation range: `(0, nothing)`
- `ext::Dict{String, Any}`
- `states::Vector{Symbol}`: The states are:
	Vr1: First integrator,
	Vr2: Second integrator
- `n_states::Int`: SCRX has 2 states
- `states_types::Vector{StateTypes}`: SCRX has 2 differential states
- `internal::InfrastructureSystemsInternal`: power system internal reference, do not modify
"""
mutable struct SCRX <: AVR
    "Lead input constant ratio"
    Ta_Tb::Float64
    "Lag input constant in s"
    Tb::Float64
    "Regulator Gain"
    K::Float64
    "Regulator Time Constant"
    Te::Float64
    "Field Voltage regulator limits (regulator output) (Efd_min, Efd_max)"
    Efd_lim::NamedTuple{(:min, :max), Tuple{Float64, Float64}}
    "Switch"
    switch::Int
    "Field current capability. Set = 0 for negative current capability. Typical value 10"
    rc_rfd::Float64
    "Reference Voltage Set-point"
    V_ref::Float64
    ext::Dict{String, Any}
    "The states are:
	Vr1: First integrator,
	Vr2: Second integrator"
    states::Vector{Symbol}
    "SCRX has 2 states"
    n_states::Int
    "SCRX has 2 differential states"
    states_types::Vector{StateTypes}
    "power system internal reference, do not modify"
    internal::InfrastructureSystemsInternal
end

function SCRX(Ta_Tb, Tb, K, Te, Efd_lim, switch, rc_rfd, V_ref=1.0, ext=Dict{String, Any}(), )
    SCRX(Ta_Tb, Tb, K, Te, Efd_lim, switch, rc_rfd, V_ref, ext, [:Vr1, :Vr2], 2, [StateTypes.Differential, StateTypes.Hybrid], InfrastructureSystemsInternal(), )
end

function SCRX(; Ta_Tb, Tb, K, Te, Efd_lim, switch, rc_rfd, V_ref=1.0, ext=Dict{String, Any}(), states=[:Vr1, :Vr2], n_states=2, states_types=[StateTypes.Differential, StateTypes.Hybrid], internal=InfrastructureSystemsInternal(), )
    SCRX(Ta_Tb, Tb, K, Te, Efd_lim, switch, rc_rfd, V_ref, ext, states, n_states, states_types, internal, )
end

# Constructor for demo purposes; non-functional.
function SCRX(::Nothing)
    SCRX(;
        Ta_Tb=0,
        Tb=0,
        K=0,
        Te=0,
        Efd_lim=(min=0.0, max=0.0),
        switch=0,
        rc_rfd=0,
        V_ref=0,
        ext=Dict{String, Any}(),
    )
end

"""Get [`SCRX`](@ref) `Ta_Tb`."""
get_Ta_Tb(value::SCRX) = value.Ta_Tb
"""Get [`SCRX`](@ref) `Tb`."""
get_Tb(value::SCRX) = value.Tb
"""Get [`SCRX`](@ref) `K`."""
get_K(value::SCRX) = value.K
"""Get [`SCRX`](@ref) `Te`."""
get_Te(value::SCRX) = value.Te
"""Get [`SCRX`](@ref) `Efd_lim`."""
get_Efd_lim(value::SCRX) = value.Efd_lim
"""Get [`SCRX`](@ref) `switch`."""
get_switch(value::SCRX) = value.switch
"""Get [`SCRX`](@ref) `rc_rfd`."""
get_rc_rfd(value::SCRX) = value.rc_rfd
"""Get [`SCRX`](@ref) `V_ref`."""
get_V_ref(value::SCRX) = value.V_ref
"""Get [`SCRX`](@ref) `ext`."""
get_ext(value::SCRX) = value.ext
"""Get [`SCRX`](@ref) `states`."""
get_states(value::SCRX) = value.states
"""Get [`SCRX`](@ref) `n_states`."""
get_n_states(value::SCRX) = value.n_states
"""Get [`SCRX`](@ref) `states_types`."""
get_states_types(value::SCRX) = value.states_types
"""Get [`SCRX`](@ref) `internal`."""
get_internal(value::SCRX) = value.internal

"""Set [`SCRX`](@ref) `Ta_Tb`."""
set_Ta_Tb!(value::SCRX, val) = value.Ta_Tb = val
"""Set [`SCRX`](@ref) `Tb`."""
set_Tb!(value::SCRX, val) = value.Tb = val
"""Set [`SCRX`](@ref) `K`."""
set_K!(value::SCRX, val) = value.K = val
"""Set [`SCRX`](@ref) `Te`."""
set_Te!(value::SCRX, val) = value.Te = val
"""Set [`SCRX`](@ref) `Efd_lim`."""
set_Efd_lim!(value::SCRX, val) = value.Efd_lim = val
"""Set [`SCRX`](@ref) `switch`."""
set_switch!(value::SCRX, val) = value.switch = val
"""Set [`SCRX`](@ref) `rc_rfd`."""
set_rc_rfd!(value::SCRX, val) = value.rc_rfd = val
"""Set [`SCRX`](@ref) `V_ref`."""
set_V_ref!(value::SCRX, val) = value.V_ref = val
"""Set [`SCRX`](@ref) `ext`."""
set_ext!(value::SCRX, val) = value.ext = val
"""Set [`SCRX`](@ref) `states_types`."""
set_states_types!(value::SCRX, val) = value.states_types = val


Network = NetLib.Gin

netparams = NetLib.GinHP()

self_play = SelfPlayParams(
  sim=SimParams(
    num_games=ConstSchedule(256),
    num_workers=ConstSchedule(200),
    batch_size=ConstSchedule(100),
    use_gpu=true,
    reset_every=1,
    flip_probability=0.,
    alternate_colors=false),
  mcts=MctsParams(
    num_iters_per_turn=300,
    cpuct=0.8,
    adaptive_cpuct = false,
    temperature=ConstSchedule(0.05),
    dirichlet_noise_ϵ=0.,
    dirichlet_noise_α=0.2))

learning = LearningParams(
  use_gpu=true,
  use_position_averaging=false,
  samples_weighing_policy=LOG_WEIGHT,
  l2_regularization=1e-5,
  optimiser=Adam(lr=1e-4),
  batch_size=2048,
  loss_computation_batch_size=2048,
  nonvalidity_penalty=0.,
  min_checkpoints_per_epoch=1,
  max_batches_per_checkpoint=2_000,
  num_checkpoints=1,
  rewards_renormalization = 1)

params = Params(
  arena=nothing,
  self_play=self_play,
  learning=learning,
  num_iters=50,
  memory_analysis=nothing,
  ternary_rewards=false,
  use_symmetries=false,
  mem_buffer_size=PLSchedule(500_000))

benchmark_sim = SimParams(
  self_play.sim;
  num_games=ConstSchedule(64),
  num_workers=ConstSchedule(44),
  batch_size=ConstSchedule(22),
  deterministic = true)

benchmark = [
  Benchmark.Single(
    Benchmark.Full(MctsParams(self_play.mcts, temperature=ConstSchedule(0.))),
    benchmark_sim),
  Benchmark.Single(
    Benchmark.NetworkOnly(),
    benchmark_sim)]

experiment = Experiment(
  "jsp", GameSpec(PLSchedule([1, 10], [2, 6])=>PLSchedule([1, 10], [4, 8]),
                  PLSchedule([1, 10], [2, 6])=>PLSchedule([1, 10], [4, 8]),
                  1=>5), params, Network, netparams, benchmark)


struct EOS_LNG <: MultiFluidModel
    components::Vector{String}
    icomponents::UnitRange{Int}
    properties::MultiFluidPropertyParam
    ideal::MultiFluidIdealParam
    single::MultiFluidSingleParam
    pair::MultiFluidPairParam
    references::Vector{String}
end

@registermodel EOS_LNG

function EOS_LNG(components::Vector{String})
    I = getnames_gerg2008(components)

    params = getparams_eos_lng()
    Mw = each_split_model(params[:Mw],I)
    rhoc = each_split_model(params[:rhoc],I)
    vc = SingleParam("critical volume (dm3/mol)",rhoc.components,1 ./ rhoc.values)
    Tc = each_split_model(params[:Tc],I)
    pc = each_split_model(params[:pc],I)
    lb_v = each_split_model(params[:lb_v],I)
    properties = MultiFluidPropertyParam(Mw,rhoc,vc,Tc,pc,lb_v)
    n0 = each_split_model(params[:n0],I)
    theta = each_split_model(params[:theta],I)
    gamma_T = each_split_model(params[:gamma_T],I)
    gamma_v = each_split_model(params[:gamma_v],I)
    beta_T = each_split_model(params[:beta_T],I)
    beta_v = each_split_model(params[:beta_v],I)
    ideal = MultiFluidIdealParam(n0,theta,gamma_T,gamma_v,beta_T,beta_v)
    n = each_split_model(params[:n],I) |> pack_vectors
    t = each_split_model(params[:t],I) |> pack_vectors
    d = each_split_model(params[:d],I) |> pack_vectors
    c = each_split_model(params[:c],I) |> pack_vectors
    single = MultiFluidSingleParam(n,t,d,c)
    nij = each_split_model(params[:nij],I) |> pack_vectors
    tij = each_split_model(params[:tij],I) |> pack_vectors
    dij = each_split_model(params[:dij],I) |> pack_vectors
    Fij = each_split_model(params[:Fij],I)
    eta_ij = each_split_model(params[:eta_ij],I) |> pack_vectors
    epsilon_ij = each_split_model(params[:epsilon_ij],I) |> pack_vectors
    beta_ij = each_split_model(params[:beta_ij],I) |> pack_vectors
    gamma_ij = each_split_model(params[:gamma_ij],I) |> pack_vectors
    pair = MultiFluidPairParam(nij,tij,dij,Fij,beta_ij,gamma_ij,eta_ij,epsilon_ij)
    icomponents = 1:length(I)
    references = ["10.1021/je300655b"]
    return EOS_LNG(components,icomponents,properties,ideal,single,pair,references)
end

export EOS_LNG
#nij tij dij ηij εij βij γij
#C1,C4

function getparams_eos_lng()
    params = getparams_gerg2008()
    γ_T =params[:gamma_T].values
    γ_v = params[:gamma_v].values
    β_T = params[:beta_T].values
    β_v = params[:beta_v].values
    lng_newpairs = [(1,6),(1,7),(1,8),(1,9)]
    _β_T = [0.9421, 0.9405, 0.9082, 0.886]
    _γ_T = [1.0307, 0.9917, 1.03884, 0.993]
    _β_v = [1.035, 1.0434, 1.02874, 1.023]
    _γ_v = [1.118, 1.143, 1.13209, 1.076]

    for (k,tup) in pairs(lng_newpairs)
        i,j = tup
        γ_T[i,j] =  _γ_T[k]
        γ_v[i,j] =  _γ_v[k]
        γ_T[j,i] =  _γ_T[k]
        γ_v[j,i] =  _γ_v[k]

        β_T[i,j] = _β_T[k]
        β_v[i,j] = _β_v[k]
        β_T[j,i] = 1/_β_T[k]
        β_v[j,i] = 1/_β_v[k]
    end

    nij = params[:nij].values
    tij = params[:tij].values
    dij = params[:dij].values
    eta_ij = params[:eta_ij].values
    epsilon_ij =params[:epsilon_ij].values
    beta_ij = params[:beta_ij].values
    gamma_ij = params[:gamma_ij].values
#C1,C4i
    nij[1,6] = [0.7588, -0.4386, -0.02273, 45.05, -2.291, -62.51, 33.32, -12.14]
    tij[1,6] = [1.02, 0.71, 1.57, 3.41, 2.12, 3.28, 3.37, 3.4]
    dij[1,6] = [1,2,3,1,1,1,2,1]
    eta_ij[1,6] = [1.34, 1.45, 0.96, 1.33, 1.9]
    epsilon_ij[1,6] = [0.59, 1.9, 0.87, 1.12, 1.43]
    beta_ij[1,6] = [1.07, 1.06, 1.11, 1.2, 1.23]
    gamma_ij[1,6] = [0.0, 0.0, 0.0, 0.0, 0.0]
#C1,C4i
    nij[1,7] = [0.9396, -0.1439, -0.1413, 35.32, -4.216, 59.17, -76.68, -41.39]
    tij[1,7] = [1.43, 0.3, 1.2, 3.1, 1.78, 3.36, 2.7, 3.7]
    dij[1,7] = [1,2,3,1,1,1,2,1]
    eta_ij[1,7] = [1.87, 1.05, 1.78, 1.19, 2.0]
    epsilon_ij[1,7] = [1.73, 0.78, 1.75, 1.84, 1.71]
    beta_ij[1,7] = [1.67, 1.76, 1.02, 1.76, 1.06]
    gamma_ij[1,7] = [0.0, 0.0, 0.0, 0.0, 0.0]

#C1C5
    nij[1,8] = [0.03711, -0.12154, 27.086, -13.614, -14.45, -0.46867]
    tij[1,8] = [1.54, 0.95, 0.47, 0.9, 0.22, 2.65]
    dij[1,8] = [2,3,1,1,1,2]
    eta_ij[1,8] = [0.6, 0.43, 0.7, 1.4]
    epsilon_ij[1,8] = [0.7, 0.61, 0.7, 0.46]
    beta_ij[1,8] = [0.916, 0.87, 0.86, 2.8]
    gamma_ij[1,8] = [0.5,0.5,0.5,0.5]
#C1C5i
    nij[1,9] =[0.051, -0.158, -67.49, -88.27, 154.9, 3.725]
    tij[1,9] = [0.2, 0.53, 1.79, 2.1, 2.0, 0.2]
    dij[1,9] = [2,3,1,1,1,2]
    eta_ij[1,9] = [0.64, 0.39, 0.48, 1.0]
    epsilon_ij[1,9] = [0.5, 0.5, 0.5, 0.5]
    beta_ij[1,9] = [1.56, 1.33, 1.46, 2.7]
    gamma_ij[1,9] = [0.0, 0.0, 0.0, 0.0]

    return params
end


abstract type AbstractOperationsModel end

mutable struct PowerOperationModel{M <: AbstractOperationsModel,
                                   T <: PM.AbstractPowerFormulation}
    op_model::Type{M}
    transmission::Type{T}
    devices::Dict{Symbol, DeviceModel}
    branches::Dict{Symbol, DeviceModel}
    services::Dict{Symbol, ServiceModel}
    system::PSY.System
    canonical_model::PSI.CanonicalModel

    function PowerOperationModel(op_model::Type{M},
                                transmission::Type{T},
                                devices::Dict{Symbol, DeviceModel},
                                branches::Dict{Symbol, DeviceModel},
                                services::Dict{Symbol, ServiceModel},
                                system::PSY.System;
                                optimizer::Union{Nothing,JuMP.OptimizerFactory}=nothing,
                                kwargs...) where {M <: AbstractOperationsModel,
                                                  T <: PM.AbstractPowerFormulation}

        ps_model = build_canonical_model(transmission,
                                        devices,
                                        branches,
                                        services,
                                        system,
                                        optimizer;
                                        kwargs...)

        new{M, T}(op_model,
                  transmission,
                  devices,
                  branches,
                  services,
                  system,
                  ps_model)

    end

end


using Powseeker
using POMDPToolbox
using POMCP
using POMCPOW
using Plots
using Parameters
using POMDPs
using ContinuousPOMDPTreeSearchExperiments
using ParticleFilters
using Reel
using ProgressMeter

# using Gallium
# Gallium.breakpoint(Pkg.dir("ContinuousPOMDPTreeSearchExperiments","src","ContinuousPOMDPTreeSearchExperiments.jl"), 68)
# @conditional Gallium.breakpoint(Pkg.dir("POMCPOW","src","solver2.jl"), 112) (h_node.node==14)

pomdp = PowseekerPOMDP()
rng_seed = 6
rng = MersenneTwister(rng_seed)

## POMCPDPW ##

rng3 = copy(rng)
action_gen = GPSFirst(rng3)
rollout_policy = Downhill(pomdp)
tree_queries = 100
updater = ObsAdaptiveParticleFilter(pomdp, LowVarianceResampler(10_000), 0.05, rng)
node_updater = ObsAdaptiveSRFilter(pomdp, LowVarianceResampler(100), 0.05, rng)
solver = POMCPDPWSolver(next_action=action_gen,
                        tree_queries=tree_queries,
                        c = exp(35.0),
                        max_depth=mdp(pomdp).duration+2,
                        k_action=8.0,
                        alpha_action=1/20,
                        k_observation=8.0,
                        alpha_observation=1/10,
                        estimate_value=FORollout(rollout_policy),
                        # node_sr_belief_updater=node_updater,
                        rng=rng3
                       )
policy = solve(solver, pomdp)

#=
ib = initial_state_distribution(pomdp)
a = action(policy, ib)
POMCP.blink(get(policy._tree_ref))
=#

#=
using ProfileView
ib = initial_state_distribution(pomdp)
a = action(policy, ib)
Profile.clear()
@profile a = action(policy, ib)
ProfileView.view()
=#

hr = HistoryRecorder(max_steps=mdp(pomdp).duration, rng=MersenneTwister(rng_seed), show_progress=true)
hist = simulate(hr, pomdp, policy, updater)
println(discounted_reward(hist))

# using JLD
# filename = tempname()*".jld"
# JLD.save(filename, "hist", hist, "solver", solver, "pomdp", pomdp)
# println("saved to "*filename)

gr()
frames = Frames(MIME("image/png"), fps=2)
@showprogress "Creating gif..." for i in 1:n_steps(hist)
    plot(pomdp)
    push!(frames, plot!(view(hist, 1:i)))
end

filename = string(tempname(), "_pomcpdpw_powseeker.gif")
write(filename, frames)
println(filename)
run(`setsid gifview $filename`)


# Greetings! from path ~/.julia/config/startup.jl

# """
#     1. Generate tmp file: find $(pwd) -name \*.sac > ./tmp.txt
#     2. Run S0A_FFT.jl script
# """

############################################
## 1.Set parameters
############################################
# processes parameters
cores = 2                                               # total number of processes. cores number should be less than data-file number.
flag = true                                             # true of false; print computing time for debugging purpose.
precompile = false                                      # true or false; precompile or not.

# absolute path parameters
FFTDIR = expanduser("./FFT")                            # dir to store FFTData.

# select sac/mseed files or not
select_file = false                                     # true of false; whether specify sac/mseed files path.

# when select_file = ture, 'TMPTXT' works
FILETXT = expanduser("./file.txt")                      # sac/mseed files path.

# when select_file = false, 'DATADIR' 'select_sta' 'STATXT' and 'ncomp' work.
DATADIR = expanduser("./DATA2")                         # absolute dir where sac/mseed files are stored.
select_sta = "sta"                                      # "no" or "sta"; fft only works for selected net-sta base on STATXT.
STATXT = expanduser("./station_fft.txt")                # station infomation including: network-name and station-name.
channel_regular = "*CHN*"                               # channel(regular expression) of sac/mseed data; we will use 'glob(comp,DATADIR)' to select data in 'DATADIR' path.

# instrument parameters (The functionality has not been added!! please use [rm_resp = "no"])
rm_resp = "no"                                          # "no", "xml", "RESP", and "PZs"; select 'no' to not remove response and use "xml", "RESP", or "PZs" to remove response
respdir = expanduser("./XX.PZs")                        # path where resp files are located (required if rm_resp != "no")

# some control parameters
input_fmt = "sac"                                       # "sac" or "mseed".
time_norm = "one_bit"                                   # "no", "one_bit", "clip", "mute", and "smooth"; "no" for no normalization in time domain.
freq_norm = "whiten"                                    # "no", "whiten", "coherence", and "deconvolution"; "no" for no whitening, "whiten" for preserving only the phase component of the signal, "coherence" for smoothing absolute value of itself, and "deconvolution" for smoothing absolute value squared of itself.
cc_method = "CC"                                        # "CC" and "PCC"; FFT transformation depends on different cross-correlation methods, "CC" for pure cross correlation, and "PCC" for Phase cross-correlation, see [Ventosa et al., 2019] for "PCC".

# pre-processing parameters
freqmin = 0.5                                           # Float64; for 'bandpass!' of raw-data and "whiten" parameter
freqmax = 20.0                                          # Float64; for 'bandpass!' of raw-data and "whiten" parameter
cc_len = 60 * 2                                         # Int64; basic unit of data length for fft (sec)
cc_step = 60 * 1                                        # Int64; overlapping between each cc_len (sec)

# criteria for data selection in time_norm
factor_clip_std = 10.0                                  # Float64; 'clip' (default value), clipping the data whose std exceeds the 'factor_clip_std' to max/min mum value. || 中文: NoisePy与之对应的参数是 'max_over_std', 其默认值设置为10, 时间序列超过10倍均方差std的时间序列，把振幅设置成上下线（最大值），限制在一个范围内.
factor_mute = 3.0                                       # Float64; 'mute' (default value), mute the data whose amp exceed 'factor_mute' times the median to 0, for suppressing high-amplitude signals. || 中文: 时间序列超过3倍于波形的平均中值(使用 hibert 变换), 把振幅设置成0. median of envelope of `A` to find outliers.
time_half_win = 3                                       # Int64; 'smooth' (default value), the length(sample points) of the window for running mean normalization, using smooth(A, half_win) -> Smooth array A with half-window half_win (defaults to 3). || 中文: 将一段时间序列分成许多小的时窗，对每一个小的时窗内的波形进行滑动平均，NoisePy对应的参数是 "rma", 其窗口长度的默认值 smooth_N=10; 这也是bensen(2007)推荐的 time_norm 的方法.

# criteria for data selection in freq_norm
freq_half_win = 20                                      # Int64; 'coherence' and 'deconvolution' (default value), the length(sample points) of the window for running mean normalization, using coherence!(F,20) or deconvolution!(F,20) -> Smooth array F with half-window half_win (defaults to 20). || 中文: 频率域内的滑动平均，NoisePy对应的参数是 "rma", 其窗口长度的默认值 smooth_N=10; NoisePy 中 time_norm and freq_norm 使用的参数是同一个，都是smooth_N

############################################
# 2.run NoiseCC.FFT_CC
############################################
# a.using and import
# using Distributed
# NoiseCC_path = "/Users/yf/3.Project/2-1.Velocity_Change/SeisNoise_Code_YF/code_pmap_thread/"
# push!(LOAD_PATH, NoiseCC_path)
# @everywhere using NoiseCC # or @everywhere import NoiseCC

# b.include
using Distributed
addprocs(cores, topology = :master_worker)
@everywhere include("/Users/yf/3.Project/2-1.Velocity_Change/SeisNoise_Code_YF/code_pmap_thread/NoiseCC.jl")

NoiseCC.FFT(
    cores,
    flag,
    precompile,
    FFTDIR,
    select_file,
    FILETXT,
    DATADIR,
    select_sta,
    STATXT,
    input_fmt,
    time_norm,
    freq_norm,
    cc_method,
    channel_regular,
    rm_resp,
    respdir,
    freqmin,
    freqmax,
    cc_len,
    cc_step,
    factor_clip_std,
    factor_mute,
    time_half_win,
    freq_half_win,
)


module Peter
  import Base.+

  struct PeterNumber <: Number
    x::Number
  end

  Base.:+(p::PeterNumber, y::Number) = PeterNumber(p.x + y)

  export PeterNumber
end

module Jarrett
  import Base.+

  struct JarrettNumber <: Number
    y::Number
  end

  Base.:+(x::Number, j::JarrettNumber) = JarrettNumber(x + j.y)

  export JarrettNumber
end

using .Peter
using .Jarrett

p = PeterNumber(1.0) + 3.0
j = 2.0 + JarrettNumber(2.0)
f = p + j

println("Peter and Jarrett are friends! $f")


module MSQR

using Yao
using QMPS
using Flux
using LinearAlgebra
using Statistics
using Requires

include("MPSSwapTest.jl")
include("MSQRtrain.jl")
include("SWAPtrain.jl")
@init @require CuYao="b48ca7a8-dd42-11e8-2b8e-1b7706800275" include("CUDA.jl")

end


# Greetings! from path ~/.julia/config/startup.jl

############################################
## 1.Set parameters
############################################
# processes and threads parameters
cores = 2                                                           # Int64; total number of processes. cores number should be less than time-chunck number corresponded with "select_time".
threads = 1                                                         # Int64; total number of threads. threads number should be less than N, N is the number of sta-pairs in each time-chunck.
flag = true                                                         # true of false; print computing time for debugging purpose.
precompile = false                                                  # true or false; precompile or not.

# absolute path parameters
FFTDIR = expanduser("/Users/yf/3.Project/2-1.Velocity_Change/FFT")  # dir where FFT data are stored.
CCFDIR = expanduser("./CORR_fft")                                   # dir to store CC data.

# select time-chunck folder for cross-correlate
select_time = false                                                 # true or false; whether specify time-chunck folder path.
TIMETXT = expanduser("./time_cc.txt")                               # time-chunck folder path.

# select station for cross
select_sta = "sr"                                                   # "no", "sta" or "sr"; "sta" cross-correlate only works for selected net-sta pairs base on STATXT; "sr" cross-correlate only works for specified source and receiver based on SRTXT.
STATXT = expanduser("./sta_cc.txt")                                 # station infomation including: network-name and station-name.
SRTXT = expanduser("./sr_cc.txt")                                   # source and receiver information including: source-1 receiver-1 r-2 r-3...\nsource-2 r-1 r-2 r-3...
channel_regular = "*CHN.*"                                          # channel(regular expression) of FFT-data; we will use 'glob(comp,FFTDIR)' to select data in 'FFTDIR' path.
comp = "CHN"                                                        # component name corresponds to "channel_regular"; we will use 'load_fft(file,comp)' to read FFTData in 'FFTDIR' path, and "comp" is the group name of jld2 file(HDF5 in julia).

# some control parameters
cc_method = "CC"                                                    # "CC" and "PCC"; "CC" for pure cross correlation, or "PCC" for Phase cross-correlation, see [Ventosa et al., 2019].
auto_corr = true                                                    # true or false; perform auto-correlation or not.
cross_corr = true                                                   # true or false; perform cross-correlation or not.

# cross-correlation parameters
maxlag = 20.0                                                       # Float64; lags of cross-correlation to save (sec).
substack = true                                                     # true or false; sub-stack one time-chunck result of cross-correlation or not.
substack_method = ["mean","pws"]                                           # "mean", "pws", and "robust"; Vector{String}, you can choose one or more like ["mean","pws","robust"].

############################################
# 2.run NoiseCC.FFT_CC
############################################
# a.using and import
# using Distributed
# NoiseCC_path = "/Users/yf/3.Project/2-1.Velocity_Change/SeisNoise_Code_YF/code_pmap_thread/"
# push!(LOAD_PATH, NoiseCC_path)
# @everywhere using NoiseCC # or @everywhere import NoiseCC

# b.include
using Distributed
addprocs(cores, topology = :master_worker, exeflags = "-t $(threads)")
@everywhere include(
    "/Users/yf/3.Project/2-1.Velocity_Change/SeisNoise_Code_YF/code_pmap_thread/NoiseCC.jl",
)

NoiseCC.CC(
    cores,
    threads,
    flag,
    precompile,
    FFTDIR,
    CCFDIR,
    select_time,
    TIMETXT,
    select_sta,
    STATXT,
    SRTXT,
    channel_regular,
    comp,
    cc_method,
    auto_corr,
    cross_corr,
    maxlag,
    substack,
    substack_method,
)


"""
    MOBIL
See Treiber & Kesting, 'Modeling Lane-Changing Decisions with MOBIL'

# Constructor
	MOBIL(timestep::Float64;rec::SceneRecord=SceneRecord(2,timestep), mlon::LaneFollowingDriver=IntelligentDriverModel(),safe_decel::Float64=2.0,       politeness::Float64=0.35,advantage_threshold::Float64=0.1)

# Fields
- `dir::Int`
- `rec::SceneRecord`
- `mlon::LaneFollowingDriver=IntelligentDriverModel()`
- `safe_decel::Float64=2.0`
- `politeness::Float64=0.35`
- `advantage_threshold::Float64=0.1`
"""
@with_kw mutable struct MOBIL <: LaneChangeModel{LaneChangeChoice}
    dir::Int64
    rec::SceneRecord
    mlon::LaneFollowingDriver
    safe_decel::Float64 # safe deceleration (positive value)
    politeness::Float64 # politeness factor (suggested p ∈ [0.2,0.5])
    advantage_threshold::Float64 # Δaₜₕ
end

function MOBIL(
    timestep::Float64;
    rec::SceneRecord=SceneRecord(2,timestep),
    mlon::LaneFollowingDriver=IntelligentDriverModel(),
    safe_decel::Float64=2.0, # [m/s²]
    politeness::Float64=0.35,
    advantage_threshold::Float64=0.1,
    )
    return MOBIL(DIR_MIDDLE, rec, mlon, safe_decel, politeness, advantage_threshold)
end

"""
Return the name of the lane changing model
"""
get_name(::MOBIL) = "MOBIL"

"""
Set the desired speed of the longitudinal model within MOBIL
"""
function set_desired_speed!(model::MOBIL, v_des::Float64)
    set_desired_speed!(model.mlon, v_des)
    model
end
function observe!(model::MOBIL, scene::Frame{Entity{S, D, I}}, roadway::Roadway, egoid::I) where {S, D, I}

    rec = model.rec
    update!(rec, scene)

    vehicle_index = findfirst(egoid, rec[0])
    veh_ego = scene[vehicle_index]
    v = vel(veh_ego.state)
    egostate_M = veh_ego.state

    left_lane_exists = convert(Float64, get(N_LANE_LEFT, rec, roadway, vehicle_index)) > 0
    right_lane_exists = convert(Float64, get(N_LANE_RIGHT, rec, roadway, vehicle_index)) > 0
    fore_M = get_neighbor_fore_along_lane(scene, vehicle_index, roadway, VehicleTargetPointFront(), VehicleTargetPointRear(), VehicleTargetPointFront())
    rear_M = get_neighbor_rear_along_lane(scene, vehicle_index, roadway, VehicleTargetPointFront(), VehicleTargetPointFront(), VehicleTargetPointRear())

    # accel if we do not make a lane change
    accel_M_orig = rand(observe!(reset_hidden_state!(model.mlon), scene, roadway, egoid)).a
    model.dir = DIR_MIDDLE

    advantage_threshold = model.advantage_threshold

    if left_lane_exists

        rear_L = get_neighbor_rear_along_left_lane(scene, vehicle_index, roadway, VehicleTargetPointFront(), VehicleTargetPointFront(), VehicleTargetPointRear())

        # candidate position after lane change is over
        footpoint = get_footpoint(veh_ego)
        lane = get_lane(roadway, veh_ego)
        lane_L = roadway[LaneTag(lane.tag.segment, lane.tag.lane + 1)]
        roadproj = proj(footpoint, lane_L, roadway)
        frenet_L = Frenet(RoadIndex(roadproj), roadway)
        egostate_L = VehicleState(frenet_L, roadway, vel(veh_ego.state))

        Δaccel_n = 0.0
        passes_safety_criterion = true
        if rear_L.ind != nothing
            id = scene[rear_L.ind].id
            accel_n_orig = rand(observe!(reset_hidden_state!(model.mlon), scene, roadway, id)).a

            # update ego state in scene
            scene[vehicle_index] = Entity(veh_ego, egostate_L)
            accel_n_test = rand(observe!(reset_hidden_state!(model.mlon), scene, roadway, id)).a

            body = inertial2body(get_rear(scene[vehicle_index]), get_front(scene[rear_L.ind])) # project ego to be relative to target
            s_gap = body.x

            # restore ego state
            scene[vehicle_index] = veh_ego
            passes_safety_criterion = accel_n_test ≥ -model.safe_decel && s_gap ≥ 0
            Δaccel_n = accel_n_test - accel_n_orig
        end

        if passes_safety_criterion

            Δaccel_o = 0.0
            if rear_M.ind != nothing
                id = scene[rear_M.ind].id
                accel_o_orig = rand(observe!(reset_hidden_state!(model.mlon), scene, roadway, id)).a

                # update ego state in scene
                scene[vehicle_index] = Entity(veh_ego, egostate_L)
                accel_o_test = rand(observe!(reset_hidden_state!(model.mlon), scene, roadway, id)).a

                # restore ego state
                scene[vehicle_index] = veh_ego
                Δaccel_o = accel_o_test - accel_o_orig
            end

            # update ego state in scene
            scene[vehicle_index] = Entity(veh_ego, egostate_L)
            accel_M_test = rand(observe!(reset_hidden_state!(model.mlon), scene, roadway, egoid)).a
            # restore ego state
            scene[vehicle_index] = veh_ego

            Δaccel_M = accel_M_test - accel_M_orig

            Δaₜₕ = Δaccel_M + model.politeness*(Δaccel_n + Δaccel_o)

            if Δaₜₕ > advantage_threshold
                model.dir = DIR_LEFT
                advantage_threshold = Δaₜₕ
            end
        end
    end

    if right_lane_exists

        rear_R = get_neighbor_rear_along_right_lane(scene, vehicle_index, roadway, VehicleTargetPointFront(), VehicleTargetPointFront(), VehicleTargetPointRear())

        # candidate position after lane change is over
        footpoint = get_footpoint(veh_ego)
        lane = roadway[veh_ego.state.posF.roadind.tag]
        lane_R = roadway[LaneTag(lane.tag.segment, lane.tag.lane - 1)]
        roadproj = proj(footpoint, lane_R, roadway)
        frenet_R = Frenet(RoadIndex(roadproj), roadway)
        egostate_R = VehicleState(frenet_R, roadway, vel(veh_ego.state))

        Δaccel_n = 0.0
        passes_safety_criterion = true
        if rear_R.ind != nothing
            id = scene[rear_R.ind].id
            accel_n_orig = rand(observe!(reset_hidden_state!(model.mlon), scene, roadway, id)).a

            # update ego vehicle in scene
            scene[vehicle_index] = Entity(veh_ego, egostate_R)
            accel_n_test = rand(observe!(reset_hidden_state!(model.mlon), scene, roadway, id)).a

            body = inertial2body(get_rear(scene[vehicle_index]), get_front(scene[rear_R.ind])) # project ego to be relative to target
            s_gap = body.x

            # restore ego vehicle state
            scene[vehicle_index] = veh_ego

            passes_safety_criterion = accel_n_test ≥ -model.safe_decel && s_gap ≥ 0
            Δaccel_n = accel_n_test - accel_n_orig
        end

        if passes_safety_criterion

            Δaccel_o = 0.0
            if rear_M.ind != nothing
                id = scene[rear_M.ind].id
                accel_o_orig = rand(observe!(reset_hidden_state!(model.mlon), scene, roadway, id)).a

                # update ego vehicle in scene
                scene[vehicle_index] = Entity(veh_ego, egostate_R)
                accel_o_test = rand(observe!(reset_hidden_state!(model.mlon), scene, roadway, id)).a
                # restore ego vehicle state
                scene[vehicle_index] = veh_ego

                Δaccel_o = accel_o_test - accel_o_orig
            end

            # update ego vehicle in scene
            scene[vehicle_index] = Entity(veh_ego, egostate_R)
            accel_M_test = rand(observe!(reset_hidden_state!(model.mlon), scene, roadway, egoid)).a
            # restor ego vehicle state
            scene[vehicle_index] = veh_ego

            Δaccel_M = accel_M_test - accel_M_orig

            Δaₜₕ = Δaccel_M + model.politeness*(Δaccel_n + Δaccel_o)
            if Δaₜₕ > advantage_threshold
                model.dir = DIR_RIGHT
                advantage_threshold = Δaₜₕ
            elseif Δaₜₕ == advantage_threshold
                # in case of tie, if we are accelerating we go left, else, right
                if Δaccel_M > model.politeness*(Δaccel_n + Δaccel_o)
                    model.dir = DIR_LEFT
                else
                    model.dir = DIR_RIGHT
                end
            end
        end
    end

    model
end
Base.rand(rng::AbstractRNG, model::MOBIL) = LaneChangeChoice(model.dir)