#####
##### Memory Buffer:
##### Datastructure to collect self-play experience
#####

"""
    TrainingSample{State}

Type of a training sample. A sample features the following fields:
- `s::State` is the state
- `π::Vector{Float64}` is the recorded MCTS policy for this position
- `z::Float64` is the discounted reward cumulated from state `s`
- `t::Float64` is the (average) number of moves remaining before the end of the game
- `n::Int` is the number of times the state `s` was recorded

As revealed by the last field `n`, several samples that correspond to the
same state can be merged, in which case the `π`, `z` and `t`
fields are averaged together.
"""
struct TrainingSample{State}
  s :: State
  π :: Vector{Float64}
  z :: Float64
  t :: Float64
  n :: Int
end

sample_state_type(::Type{<:TrainingSample{S}}) where S = S

"""
    MemoryBuffer{State}

A circular buffer to hold memory samples.

# Constructor

    MemoryBuffer{State}(size, experience=[])

"""
mutable struct MemoryBuffer{State}
  buf :: CircularBuffer{TrainingSample{State}}
  cur_batch_size :: Int
  function MemoryBuffer{S}(size, experience=[]) where S
    buf = CircularBuffer{TrainingSample{S}}(size)
    append!(buf, experience)
    new{S}(buf, 0)
  end
end

"""
    get_experience(::MemoryBuffer{S}) where S :: Vector{TrainingSample{S}}

Return all samples in the memory buffer.
"""
get_experience(mem::MemoryBuffer) = mem.buf[:]

last_batch(mem::MemoryBuffer) = mem.buf[end-cur_batch_size(mem)+1:end]

cur_batch_size(mem::MemoryBuffer) = min(mem.cur_batch_size, length(mem))

new_batch!(mem::MemoryBuffer) = (mem.cur_batch_size = 0)

function Base.empty!(mem::MemoryBuffer)
  empty!(mem.buf)
  mem.cur_batch_size = 0
end

Base.length(mem::MemoryBuffer) = length(mem.buf)

"""
    push_game!(mem::MemoryBuffer, trace::Trace, gamma)

Collect samples out of a game trace and add them to the memory buffer.

Here, `gamma` is the reward discount factor.
"""
function push_game!(mem::MemoryBuffer, trace, gamma)
  n = length(trace)
  wr = 0.
  for i in reverse(1:n)
    wr = gamma * wr + trace.rewards[i]
    s = trace.states[i]
    π = trace.policies[i]
    wp = GI.white_playing(GameType(trace), s)
    z = wp ? wr : -wr
    t = float(n - i + 1)
    push!(mem.buf, TrainingSample(s, π, z, t, 1))
  end
  mem.cur_batch_size += n
end

function merge_samples(samples)
  s = samples[1].s
  π = mean(e.π for e in samples)
  z = mean(e.z for e in samples)
  n = sum(e.n for e in samples)
  t = mean(e.t for e in samples)
  return eltype(samples)(s, π, z, t, n)
end

# Merge samples that correspond to identical states
function merge_by_state(samples)
  Sample = eltype(samples)
  State = sample_state_type(Sample)
  dict = Dict{State, Vector{Sample}}()
  sizehint!(dict, length(samples))
  for s in samples
    if haskey(dict, s.s)
      push!(dict[s.s], s)
    else
      dict[s.s] = [s]
    end
  end
  return [merge_samples(ss) for ss in values(dict)]
end

function apply_symmetry(Game, sample, (symstate, aperm))
  mask = GI.actions_mask(Game(sample.s))
  symmask = GI.actions_mask(Game(symstate))
  π = zeros(eltype(sample.π), length(mask))
  π[mask] = sample.π
  π = π[aperm]
  @assert iszero(π[.~symmask])
  π = π[symmask]
  return typeof(sample)(
    symstate, π, sample.z, sample.t, sample.n)
end

function augment_with_symmetries(Game, samples)
  symsamples = [apply_symmetry(Game, s, sym)
    for s in samples for sym in GI.symmetries(Game, s.s)]
  return [samples ; symsamples]
end


__precompile__(true)

module ColorVectorSpace

using Colors, FixedPointNumbers, SpecialFunctions

import Base: ==, +, -, *, /, ^, <, ~
import Base: abs, abs2, clamp, convert, copy, div, eps, isfinite, isinf,
    isnan, isless, length, mapreduce, oneunit,
    promote_op, promote_rule, zero, trunc, floor, round, ceil, bswap,
    mod, rem, atan, hypot, max, min, real, typemin, typemax
import LinearAlgebra: norm
import StatsBase: histrange, varm
import SpecialFunctions: gamma, lgamma, lfact
import Statistics: middle

export nan

# The unaryOps
import Base:      conj, sin, cos, tan, sinh, cosh, tanh,
                  asin, acos, atan, asinh, acosh, atanh,
                  sec, csc, cot, asec, acsc, acot,
                  sech, csch, coth, asech, acsch, acoth,
                  sinc, cosc, cosd, cotd, cscd, secd,
                  sind, tand, acosd, acotd, acscd, asecd,
                  asind, atand, rad2deg, deg2rad,
                  log, log2, log10, log1p, exponent, exp,
                  exp2, exp10, expm1, cbrt, sqrt,
                  significand,
                  frexp, modf,
                  float

export dotc

# TODO: we get rid of these definitions or move them to ColorTypes.jl
TransparentRGBFloat{C<:AbstractRGB,T<:AbstractFloat} = TransparentColor{C,T,4}
TransparentGrayFloat{C<:AbstractGray,T<:AbstractFloat} = TransparentColor{C,T,2}
TransparentRGBNormed{C<:AbstractRGB,T<:Normed} = TransparentColor{C,T,4}
TransparentGrayNormed{C<:AbstractGray,T<:Normed} = TransparentColor{C,T,2}

MathTypes{T,C} = Union{AbstractRGB{T},TransparentRGB{C,T},AbstractGray{T},TransparentGray{C,T}}

# convert(RGB{Float32}, NaN) doesn't and shouldn't work, so we need to reintroduce nan
nan(::Type{T}) where {T<:AbstractFloat} = convert(T, NaN)
nan(::Type{C}) where {C<:MathTypes} = _nan(eltype(C), C)
_nan(::Type{T}, ::Type{C}) where {T<:AbstractFloat,C<:AbstractGray} = (x = convert(T, NaN); C(x))
_nan(::Type{T}, ::Type{C}) where {T<:AbstractFloat,C<:TransparentGray} = (x = convert(T, NaN); C(x,x))
_nan(::Type{T}, ::Type{C}) where {T<:AbstractFloat,C<:AbstractRGB} = (x = convert(T, NaN); C(x,x,x))
_nan(::Type{T}, ::Type{C}) where {T<:AbstractFloat,C<:TransparentRGB} = (x = convert(T, NaN); C(x,x,x,x))

## Generic algorithms
mapreduce(f, op::Union{typeof(&), typeof(|)}, a::MathTypes) = f(a)  # ambiguity
mapreduce(f, op, a::MathTypes) = f(a)
Base.add_sum(c1::MathTypes,c2::MathTypes) = mapc(Base.add_sum, c1, c2)
Base.reduce_first(::typeof(Base.add_sum), c::MathTypes) = mapc(x->Base.reduce_first(Base.add_sum, x), c)
function Base.reduce_empty(::typeof(Base.add_sum), ::Type{T}) where {T<:MathTypes}
    z = Base.reduce_empty(Base.add_sum, eltype(T))
    return zero(base_colorant_type(T){typeof(z)})
end

for f in (:trunc, :floor, :round, :ceil, :eps, :bswap)
    @eval $f(g::Gray{T}) where {T} = Gray{T}($f(gray(g)))
end
eps(::Type{Gray{T}}) where {T} = Gray(eps(T))

for f in (:trunc, :floor, :round, :ceil)
    @eval $f(::Type{T}, g::Gray) where {T<:Integer} = Gray{T}($f(T, gray(g)))
end

for f in (:mod, :rem, :mod1)
    @eval $f(x::Gray, m::Gray) = Gray($f(gray(x), gray(m)))
end

# Real values are treated like grays
ColorTypes.gray(x::Real) = x

dotc(x::T, y::T) where {T<:Real} = acc(x)*acc(y)
dotc(x::Real, y::Real) = dotc(promote(x, y)...)

# Return types for arithmetic operations
multype(::Type{A}, ::Type{B}) where {A,B} = coltype(typeof(zero(A)*zero(B)))
sumtype(::Type{A}, ::Type{B}) where {A,B} = coltype(typeof(zero(A)+zero(B)))
divtype(::Type{A}, ::Type{B}) where {A,B} = coltype(typeof(zero(A)/oneunit(B)))
powtype(::Type{A}, ::Type{B}) where {A,B} = coltype(typeof(zero(A)^zero(B)))
multype(a::Colorant, b::Colorant) = multype(eltype(a),eltype(b))
sumtype(a::Colorant, b::Colorant) = sumtype(eltype(a),eltype(b))
divtype(a::Colorant, b::Colorant) = divtype(eltype(a),eltype(b))
powtype(a::Colorant, b::Colorant) = powtype(eltype(a),eltype(b))

coltype(::Type{T}) where {T<:Fractional} = T
coltype(::Type{T}) where {T}             = Float64

acctype(::Type{T}) where {T<:FixedPoint} = FixedPointNumbers.floattype(T)
acctype(::Type{T}) where {T<:Number} = T

acc(x::Number) = convert(acctype(typeof(x)), x)

# Scalar binary RGB operations require the same RGB type for each element,
# otherwise we don't know which to return
color_rettype(::Type{A}, ::Type{B}) where {A<:AbstractRGB,B<:AbstractRGB} = _color_rettype(base_colorant_type(A), base_colorant_type(B))
color_rettype(::Type{A}, ::Type{B}) where {A<:AbstractGray,B<:AbstractGray} = _color_rettype(base_colorant_type(A), base_colorant_type(B))
color_rettype(::Type{A}, ::Type{B}) where {A<:TransparentRGB,B<:TransparentRGB} = _color_rettype(base_colorant_type(A), base_colorant_type(B))
color_rettype(::Type{A}, ::Type{B}) where {A<:TransparentGray,B<:TransparentGray} = _color_rettype(base_colorant_type(A), base_colorant_type(B))
_color_rettype(::Type{A}, ::Type{B}) where {A<:Colorant,B<:Colorant} = error("binary operation with $A and $B, return type is ambiguous")
_color_rettype(::Type{C}, ::Type{C}) where {C<:Colorant} = C

color_rettype(c1::Colorant, c2::Colorant) = color_rettype(typeof(c1), typeof(c2))

arith_colorant_type(::C) where {C<:Colorant} = arith_colorant_type(C)
arith_colorant_type(::Type{C}) where {C<:Colorant} = base_colorant_type(C)
arith_colorant_type(::Type{Gray24}) = Gray
arith_colorant_type(::Type{AGray32}) = AGray
arith_colorant_type(::Type{RGB24}) = RGB
arith_colorant_type(::Type{ARGB32}) = ARGB

## Math on Colors. These implementations encourage inlining and,
## for the case of Normed types, nearly halve the number of multiplications (for RGB)

# Scalar RGB
copy(c::AbstractRGB) = c
(+)(c::AbstractRGB) = mapc(+, c)
(+)(c::TransparentRGB) = mapc(+, c)
(-)(c::AbstractRGB) = mapc(-, c)
(-)(c::TransparentRGB) = mapc(-, c)
(*)(f::Real, c::AbstractRGB) = arith_colorant_type(c){multype(typeof(f),eltype(c))}(f*red(c), f*green(c), f*blue(c))
(*)(f::Real, c::TransparentRGB) = arith_colorant_type(c){multype(typeof(f),eltype(c))}(f*red(c), f*green(c), f*blue(c), f*alpha(c))
function (*)(f::Real, c::AbstractRGB{T}) where T<:Normed
    fs = f*(1/reinterpret(oneunit(T)))
    arith_colorant_type(c){multype(typeof(f),T)}(fs*reinterpret(red(c)), fs*reinterpret(green(c)), fs*reinterpret(blue(c)))
end
function (*)(f::Normed, c::AbstractRGB{T}) where T<:Normed
    fs = reinterpret(f)*(1/widen(reinterpret(oneunit(T)))^2)
    arith_colorant_type(c){multype(typeof(f),T)}(fs*reinterpret(red(c)), fs*reinterpret(green(c)), fs*reinterpret(blue(c)))
end
function (/)(c::AbstractRGB{T}, f::Real) where T<:Normed
    fs = (one(f)/reinterpret(oneunit(T)))/f
    arith_colorant_type(c){divtype(typeof(f),T)}(fs*reinterpret(red(c)), fs*reinterpret(green(c)), fs*reinterpret(blue(c)))
end
function (/)(c::AbstractRGB{T}, f::Integer) where T<:Normed
    fs = (1/reinterpret(oneunit(T)))/f
    arith_colorant_type(c){divtype(typeof(f),T)}(fs*reinterpret(red(c)), fs*reinterpret(green(c)), fs*reinterpret(blue(c)))
end
(+)(a::AbstractRGB{S}, b::AbstractRGB{T}) where {S,T} = color_rettype(a, b){sumtype(S,T)}(red(a)+red(b), green(a)+green(b), blue(a)+blue(b))
(-)(a::AbstractRGB{S}, b::AbstractRGB{T}) where {S,T} = color_rettype(a, b){sumtype(S,T)}(red(a)-red(b), green(a)-green(b), blue(a)-blue(b))
(+)(a::TransparentRGB, b::TransparentRGB) =
    color_rettype(a, b){sumtype(a,b)}(red(a)+red(b), green(a)+green(b), blue(a)+blue(b), alpha(a)+alpha(b))
(-)(a::TransparentRGB, b::TransparentRGB) =
    color_rettype(a, b){sumtype(a,b)}(red(a)-red(b), green(a)-green(b), blue(a)-blue(b), alpha(a)-alpha(b))
(*)(c::AbstractRGB, f::Real) = (*)(f, c)
(*)(c::TransparentRGB, f::Real) = (*)(f, c)
(/)(c::AbstractRGB, f::Real) = (one(f)/f)*c
(/)(c::TransparentRGB, f::Real) = (one(f)/f)*c
(/)(c::AbstractRGB, f::Integer) = (one(eltype(c))/f)*c
(/)(c::TransparentRGB, f::Integer) = (one(eltype(c))/f)*c

isfinite(c::Colorant{T}) where {T<:Normed} = true
isfinite(c::Colorant) = mapreducec(isfinite, &, true, c)
isnan(c::Colorant{T}) where {T<:Normed} = false
isnan(c::Colorant) = mapreducec(isnan, |, false, c)
isinf(c::Colorant{T}) where {T<:Normed} = false
isinf(c::Colorant) = mapreducec(isinf, |, false, c)
abs(c::AbstractRGB) = abs(red(c))+abs(green(c))+abs(blue(c)) # should this have a different name?
abs(c::AbstractRGB{T}) where {T<:Normed} = Float32(red(c))+Float32(green(c))+Float32(blue(c)) # should this have a different name?
abs(c::TransparentRGB) = abs(red(c))+abs(green(c))+abs(blue(c))+abs(alpha(c)) # should this have a different name?
abs(c::TransparentRGB{T}) where {T<:Normed} = Float32(red(c))+Float32(green(c))+Float32(blue(c))+Float32(alpha(c)) # should this have a different name?
abs2(c::AbstractRGB) = red(c)^2+green(c)^2+blue(c)^2
abs2(c::AbstractRGB{T}) where {T<:Normed} = Float32(red(c))^2+Float32(green(c))^2+Float32(blue(c))^2
abs2(c::TransparentRGB) = (ret = abs2(color(c)); ret + convert(typeof(ret), alpha(c))^2)
norm(c::AbstractRGB) = sqrt(abs2(c))
norm(c::TransparentRGB) = sqrt(abs2(c))

oneunit(::Type{C}) where {C<:AbstractRGB}     = C(1,1,1)
oneunit(::Type{C}) where {C<:TransparentRGB}  = C(1,1,1,1)

zero(::Type{C}) where {C<:AbstractRGB}    = C(0,0,0)
zero(::Type{C}) where {C<:TransparentRGB} = C(0,0,0,0)
zero(::Type{C}) where {C<:YCbCr} = C(0,0,0)
zero(::Type{C}) where {C<:HSV} = C(0,0,0)
oneunit(p::Colorant) = oneunit(typeof(p))
Base.one(c::Colorant) = Base.one(typeof(c))
zero(p::Colorant) = zero(typeof(p))

# These constants come from squaring the conversion to grayscale
# (rec601 luma), and normalizing
dotc(x::T, y::T) where {T<:AbstractRGB} = 0.200f0 * acc(red(x))*acc(red(y)) + 0.771f0 * acc(green(x))*acc(green(y)) + 0.029f0 * acc(blue(x))*acc(blue(y))
dotc(x::AbstractRGB, y::AbstractRGB) = dotc(promote(x, y)...)

# Scalar Gray
copy(c::AbstractGray) = c
const unaryOps = (:~, :conj, :abs,
                  :sin, :cos, :tan, :sinh, :cosh, :tanh,
                  :asin, :acos, :atan, :asinh, :acosh, :atanh,
                  :sec, :csc, :cot, :asec, :acsc, :acot,
                  :sech, :csch, :coth, :asech, :acsch, :acoth,
                  :sinc, :cosc, :cosd, :cotd, :cscd, :secd,
                  :sind, :tand, :acosd, :acotd, :acscd, :asecd,
                  :asind, :atand, :rad2deg, :deg2rad,
                  :log, :log2, :log10, :log1p, :exponent, :exp,
                  :exp2, :exp10, :expm1, :cbrt, :sqrt,
                  :significand, :lgamma,
                  :gamma, :lfact, :frexp, :modf,
                  :(SpecialFunctions.erf), :(SpecialFunctions.erfc),
                  :(SpecialFunctions.erfcx), :(SpecialFunctions.erfi), :(SpecialFunctions.dawson),
                  :(SpecialFunctions.airy), :(SpecialFunctions.airyai),
                  :(SpecialFunctions.airyprime), :(SpecialFunctions.airyaiprime), :(SpecialFunctions.airybi), :(SpecialFunctions.airybiprime),
                  :(SpecialFunctions.besselj0), :(SpecialFunctions.besselj1), :(SpecialFunctions.bessely0), :(SpecialFunctions.bessely1),
                  :(SpecialFunctions.eta), :(SpecialFunctions.zeta), :(SpecialFunctions.digamma))
for op in unaryOps
    @eval ($op)(c::AbstractGray) = $op(gray(c))
end

middle(c::AbstractGray) = arith_colorant_type(c)(middle(gray(c)))
middle(x::C, y::C) where {C<:AbstractGray} = arith_colorant_type(C)(middle(gray(x), gray(y)))

(*)(f::Real, c::AbstractGray) = arith_colorant_type(c){multype(typeof(f),eltype(c))}(f*gray(c))
(*)(f::Real, c::TransparentGray) = arith_colorant_type(c){multype(typeof(f),eltype(c))}(f*gray(c), f*alpha(c))
(*)(c::AbstractGray, f::Real) = (*)(f, c)
(*)(c::TransparentGray, f::Real) = (*)(f, c)
(/)(c::AbstractGray, f::Real) = (one(f)/f)*c
(/)(n::Number, c::AbstractGray) = n/gray(c)
(/)(c::TransparentGray, f::Real) = (one(f)/f)*c
(/)(c::AbstractGray, f::Integer) = (one(eltype(c))/f)*c
(/)(c::TransparentGray, f::Integer) = (one(eltype(c))/f)*c
(+)(a::AbstractGray{S}, b::AbstractGray{T}) where {S,T} = color_rettype(a,b){sumtype(S,T)}(gray(a)+gray(b))
(+)(a::TransparentGray, b::TransparentGray) = color_rettype(a,b){sumtype(eltype(a),eltype(b))}(gray(a)+gray(b),alpha(a)+alpha(b))
(-)(a::AbstractGray{S}, b::AbstractGray{T}) where {S,T} = color_rettype(a,b){sumtype(S,T)}(gray(a)-gray(b))
(-)(a::TransparentGray, b::TransparentGray) = color_rettype(a,b){sumtype(eltype(a),eltype(b))}(gray(a)-gray(b),alpha(a)-alpha(b))
(*)(a::AbstractGray{S}, b::AbstractGray{T}) where {S,T} = color_rettype(a,b){multype(S,T)}(gray(a)*gray(b))
(^)(a::AbstractGray{S}, b::Integer) where {S} = arith_colorant_type(a){powtype(S,Int)}(gray(a)^convert(Int,b))
(^)(a::AbstractGray{S}, b::Real) where {S} = arith_colorant_type(a){powtype(S,typeof(b))}(gray(a)^b)
(+)(c::AbstractGray) = c
(+)(c::TransparentGray) = c
(-)(c::AbstractGray) = typeof(c)(-gray(c))
(-)(c::TransparentGray) = typeof(c)(-gray(c),-alpha(c))
(/)(a::AbstractGray, b::AbstractGray) = gray(a)/gray(b)
div(a::AbstractGray, b::AbstractGray) = div(gray(a), gray(b))
(+)(a::AbstractGray, b::Number) = gray(a)+b
(-)(a::AbstractGray, b::Number) = gray(a)-b
(+)(a::Number, b::AbstractGray) = a+gray(b)
(-)(a::Number, b::AbstractGray) = a-gray(b)
max(a::T, b::T) where {T<:AbstractGray} = T(max(gray(a),gray(b)))
max(a::AbstractGray, b::AbstractGray) = max(promote(a,b)...)
max(a::Number, b::AbstractGray) = max(promote(a,b)...)
max(a::AbstractGray, b::Number) = max(promote(a,b)...)
min(a::T, b::T) where {T<:AbstractGray} = T(min(gray(a),gray(b)))
min(a::AbstractGray, b::AbstractGray) = min(promote(a,b)...)
min(a::Number, b::AbstractGray) = min(promote(a,b)...)
min(a::AbstractGray, b::Number) = min(promote(a,b)...)

norm(c::AbstractGray) = abs(gray(c))
abs(c::TransparentGray) = abs(gray(c))+abs(alpha(c)) # should this have a different name?
abs(c::TransparentGrayNormed) = Float32(gray(c)) + Float32(alpha(c)) # should this have a different name?
abs2(c::AbstractGray) = gray(c)^2
abs2(c::AbstractGray{T}) where {T<:Normed} = Float32(gray(c))^2
abs2(c::TransparentGray) = gray(c)^2+alpha(c)^2
abs2(c::TransparentGrayNormed) = Float32(gray(c))^2 + Float32(alpha(c))^2
atan(x::Gray, y::Gray) = atan(convert(Real, x), convert(Real, y))
hypot(x::Gray, y::Gray) = hypot(convert(Real, x), convert(Real, y))
norm(c::TransparentGray) = sqrt(abs2(c))

(<)(g1::AbstractGray, g2::AbstractGray) = gray(g1) < gray(g2)
(<)(c::AbstractGray, r::Real) = gray(c) < r
(<)(r::Real, c::AbstractGray) = r < gray(c)
isless(g1::AbstractGray, g2::AbstractGray) = isless(gray(g1), gray(g2))
isless(c::AbstractGray, r::Real) = isless(gray(c), r)
isless(r::Real, c::AbstractGray) = isless(r, gray(c))

function Base.isapprox(x::AbstractArray{Cx},
                       y::AbstractArray{Cy};
                       rtol::Real=Base.rtoldefault(eltype(Cx),eltype(Cy),0),
                       atol::Real=0,
                       norm::Function=norm) where {Cx<:MathTypes,Cy<:MathTypes}
    d = norm(x - y)
    if isfinite(d)
        return d <= atol + rtol*max(norm(x), norm(y))
    else
        # Fall back to a component-wise approximate comparison
        return all(ab -> isapprox(ab[1], ab[2]; rtol=rtol, atol=atol), zip(x, y))
    end
end

zero(::Type{C}) where {C<:TransparentGray} = C(0,0)
oneunit(::Type{C}) where {C<:TransparentGray} = C(1,1)

dotc(x::T, y::T) where {T<:AbstractGray} = acc(gray(x))*acc(gray(y))
dotc(x::AbstractGray, y::AbstractGray) = dotc(promote(x, y)...)

float(::Type{T}) where {T<:Gray} = typeof(float(zero(T)))

# Mixed types
(+)(a::MathTypes, b::MathTypes) = (+)(promote(a, b)...)
(-)(a::MathTypes, b::MathTypes) = (-)(promote(a, b)...)

# Arrays---necessary methods
+(A::AbstractArray{C}) where {C<:MathTypes} = A
+(A::Array{C}) where {C<:MathTypes} = A

varm(v::AbstractArray{C}, s::AbstractGray; corrected::Bool=true) where {C<:AbstractGray} =
        varm(map(gray,v),gray(s); corrected=corrected)
real(::Type{C}) where {C<:AbstractGray} = real(eltype(C))

#histrange for Gray type
histrange(v::AbstractArray{Gray{T}}, n::Integer) where {T} = histrange(convert(Array{Float32}, map(gray, v)), n, :right)

# To help type inference
promote_rule(::Type{T}, ::Type{C}) where {T<:Real,C<:AbstractGray} = promote_type(T, eltype(C))

typemin(::Type{T}) where {T<:ColorTypes.AbstractGray} = T(typemin(eltype(T)))
typemax(::Type{T}) where {T<:ColorTypes.AbstractGray} = T(typemax(eltype(T)))

typemin(::T) where {T<:ColorTypes.AbstractGray} = T(typemin(eltype(T)))
typemax(::T) where {T<:ColorTypes.AbstractGray} = T(typemax(eltype(T)))

## Deprecations

@deprecate (+)(A::AbstractArray{CV}, b::AbstractRGB) where {CV<:AbstractRGB} (.+)(A, b)
@deprecate (+)(b::AbstractRGB, A::AbstractArray{CV}) where {CV<:AbstractRGB} (.+)(b, A)
@deprecate (-)(A::AbstractArray{CV}, b::AbstractRGB) where {CV<:AbstractRGB} (.-)(A, b)
@deprecate (-)(b::AbstractRGB, A::AbstractArray{CV}) where {CV<:AbstractRGB} (.-)(b, A)
@deprecate (*)(A::AbstractArray{T}, b::AbstractRGB) where {T<:Number} A.*b
@deprecate (*)(b::AbstractRGB, A::AbstractArray{T}) where {T<:Number} A.*b

@deprecate (+)(A::AbstractArray{CV}, b::TransparentRGB) where {CV<:TransparentRGB} (.+)(A, b)
@deprecate (+)(b::TransparentRGB, A::AbstractArray{CV}) where {CV<:TransparentRGB} (.+)(b, A)
@deprecate (-)(A::AbstractArray{CV}, b::TransparentRGB) where {CV<:TransparentRGB} (.-)(A, b)
@deprecate (-)(b::TransparentRGB, A::AbstractArray{CV}) where {CV<:TransparentRGB} (.-)(b, A)
@deprecate (*)(A::AbstractArray{T}, b::TransparentRGB) where {T<:Number} A.*b
@deprecate (*)(b::TransparentRGB, A::AbstractArray{T}) where {T<:Number} A.*b

@deprecate (+)(A::AbstractArray{CV}, b::AbstractGray) where {CV<:AbstractGray} (.+)(A, b)
@deprecate (+)(b::AbstractGray, A::AbstractArray{CV}) where {CV<:AbstractGray} (.+)(b, A)
@deprecate (-)(A::AbstractArray{CV}, b::AbstractGray) where {CV<:AbstractGray} (.-)(A, b)
@deprecate (-)(b::AbstractGray, A::AbstractArray{CV}) where {CV<:AbstractGray} (.-)(b, A)
@deprecate (*)(A::AbstractArray{T}, b::AbstractGray) where {T<:Number} A.*b
@deprecate (*)(b::AbstractGray, A::AbstractArray{T}) where {T<:Number} A.*b
@deprecate (/)(A::AbstractArray{C}, b::AbstractGray) where {C<:AbstractGray} A./b

@deprecate (+)(A::AbstractArray{CV}, b::TransparentGray) where {CV<:TransparentGray} (.+)(A, b)
@deprecate (+)(b::TransparentGray, A::AbstractArray{CV}) where {CV<:TransparentGray} (.+)(b, A)
@deprecate (-)(A::AbstractArray{CV}, b::TransparentGray) where {CV<:TransparentGray} (.-)(A, b)
@deprecate (-)(b::TransparentGray, A::AbstractArray{CV}) where {CV<:TransparentGray} (.-)(b, A)
@deprecate (*)(A::AbstractArray{T}, b::TransparentGray) where {T<:Number} A.*b
@deprecate (*)(b::TransparentGray, A::AbstractArray{T}) where {T<:Number} A.*b

end


export
    TranslateFilter

mutable struct TranslateFilter <: Ranger.AbstractNode
    base::Nodes.NodeData
    transform::Nodes.TransformProperties{Float64}

    # Collection of nodes.
    children::Array{Ranger.AbstractNode,1}

    # This node's parent translation component
    translate_component::Math.AffineTransform{Float64}

    function TranslateFilter(world::Ranger.World, name::String, parent::Ranger.AbstractNode)
        o = new()

        o.base = Nodes.NodeData(name, parent, world)
        o.transform = Nodes.TransformProperties{Float64}()
        o.children = Array{Ranger.AbstractNode,1}[]
        o.translate_component = Math.AffineTransform{Float64}()

        o
    end
end

# Filters are special in that they overload the visit() method
# Because this is a translate filter we "filter out" everything
# but translation component from the immediate parent.
function Nodes.visit(node::TranslateFilter, context::Rendering.RenderContext, interpolation::Float64)
    if !Nodes.is_visible(node)
        return;
    end

    Rendering.save!(context)

    children = Nodes.get_children(node)
    if children ≠ nothing
        for child in children
            Rendering.save!(context)

            if Nodes.has_parent(node)
                parent = node.base.parent
                inverse = parent.transform.inverse

                # This removes the immediate parent's transform effects
                Rendering.apply!(context, inverse)

                # Re-introduce only the parent's translation component by
                # excluding the other components
                Nodes.calc_filtered_transform!(parent.transform, false, true, true, node.translate_component)

                Rendering.apply!(context, node.translate_component)
            else
                println("TranslateFilter::visit: ", node, " has NO parent")
                return;
            end

            # Now visit the child with the modified context
            Nodes.visit(child, context, interpolation)

            Rendering.restore!(context)
        end
    end

    Rendering.restore!(context)
end

# --------------------------------------------------------
# Grouping
# --------------------------------------------------------
function Nodes.get_children(node::TranslateFilter)
    node.children
end


# ------------------------------------------------------------------
# Licensed under the ISC License. See LICENSE in the project root.
# ------------------------------------------------------------------

"""
    EstimationProblem(sdata, sdomain, targetvars)

A spatial estimation problem on a given spatial domain `sdomain`
in which the variables to be estimated are listed in `targetvars`.
The data of the problem is stored in spatial data `sdata`.

## Examples

Create an estimation problem for rainfall precipitation measurements:

```julia
julia> EstimationProblem(sdata, sdomain, :precipitation)
```

Create an estimation problem for precipitation and CO₂:

```julia
julia> EstimationProblem(sdata, sdomain, (:precipitation,:CO₂))
```
"""
struct EstimationProblem{S,D,M} <: AbstractProblem
  # input fields
  sdata::S
  sdomain::D
  mapper::M
  targetvars::Dict{Symbol,DataType}

  # state fields
  mappings::Dict{Symbol,Dict{Int,Int}}

  function EstimationProblem{S,D,M}(sdata, sdomain, targetvars, mapper) where {S,D,M}
    probvnames = Tuple(keys(targetvars))
    datavnames = name.(variables(sdata))

    @assert !isempty(probvnames) && probvnames ⊆ datavnames "target variables must be present in spatial data"
    @assert coordtype(sdata) == coordtype(sdomain) "data and domain must have the same coordinate type"

    mappings = map(sdata, sdomain, probvnames, mapper)

    new(sdata, sdomain, mapper, targetvars, mappings)
  end
end

function EstimationProblem(sdata::S, sdomain::D, targetvarnames::NTuple;
                           mapper::M=NearestMapper()) where {S,D,M}
  # build dictionary of target variables
  vars = filter(v -> name(v) ∈ targetvarnames, variables(sdata))
  targetvars = Dict(name(var) => mactype(var) for var in vars)

  EstimationProblem{S,D,M}(sdata, sdomain, targetvars, mapper)
end

EstimationProblem(sdata, sdomain, targetvarname::Symbol; mapper=NearestMapper()) =
  EstimationProblem(sdata, sdomain, (targetvarname,); mapper=mapper)

"""
    data(problem)

Return the spatial data of the estimation `problem`.
"""
data(problem::EstimationProblem) = problem.sdata

"""
    domain(problem)

Return the spatial domain of the estimation `problem`.
"""
domain(problem::EstimationProblem) = problem.sdomain

"""
    mapper(problem)

Return the mapper of the estimation `problem`.
"""
mapper(problem::EstimationProblem) = problem.mapper

"""
    variables(problem)

Return the variable names of the estimation `problem` and their types.
"""
variables(problem::EstimationProblem) = problem.targetvars

"""
    datamap(problem, targetvar)

Return the mapping from domain locations to data locations for the
`targetvar` of the `problem`.
"""
datamap(problem::EstimationProblem, var::Symbol) = problem.mappings[var]

"""
    datamap(problem)

Return the mappings from domain locations to data locations for all
the variables of the `problem`.
"""
datamap(problem::EstimationProblem) = problem.mappings

# ------------
# IO methods
# ------------
function Base.show(io::IO, problem::EstimationProblem)
  N = ncoords(problem.sdomain)
  print(io, "$(N)D EstimationProblem")
end

function Base.show(io::IO, ::MIME"text/plain", problem::EstimationProblem)
  vars = ["$var ($T)" for (var,T) in problem.targetvars]
  println(io, problem)
  println(io, "  data:      ", problem.sdata)
  println(io, "  domain:    ", problem.sdomain)
  print(  io, "  variables: ", join(vars, ", ", " and "))
end


# Note that this script can accept some limited command-line arguments, run
# `julia build_tarballs.jl --help` to see a usage message.
using BinaryBuilder

name = "qlopt"
version = v"0.1.0"

# Collection of sources required to build qlopt
sources = [
    "https://github.com/jgoldfar/qlopt.git" =>
    "87c839112d76a7957dfcbdc09491acdce92cdba4",

    "https://dl.bintray.com/boostorg/release/1.69.0/source/boost_1_69_0.tar.gz" =>
    "9a2c2819310839ea373f42d69e733c339b4e9a19deab6bfec448281554aa4dbb",

    "http://bitbucket.org/eigen/eigen/get/3.3.7.tar.gz" =>
    "7e84ef87a07702b54ab3306e77cea474f56a40afa1c0ab245bb11725d006d0da",

]

# Bash recipe for building across all platforms
script = raw"""
# Boost build
cd $WORKSPACE/srcdir/boost_1_69_0/
./bootstrap.sh --with-libraries="system,math,test" --prefix=$prefix
./b2 -d0 -q --with-math --with-test --with-system install

# Eigen build
cd $WORKSPACE/srcdir/eigen-eigen-323c052e1731/
mkdir build && cd build/
cmake -DCMAKE_INSTALL_PREFIX=$prefix -DCMAKE_TOOLCHAIN_FILE=/opt/$target/$target.toolchain ..
make install

# QLopt build
cd $WORKSPACE/srcdir/qlopt/
make all CXXFLAGS_EXTRA="-I${prefix}/include"
# Install script. TODO: Add install target
mkdir -p $prefix/bin
mv bin/* $prefix/bin
mkdir -p $prefix/lib
mv build/*.a $prefix/lib/
exit

"""

# These are the platforms we will build for by default, unless further
# platforms are passed in on the command line
platforms = [
    Linux(:i686, libc=:glibc),
    Linux(:x86_64, libc=:glibc),
    Linux(:x86_64, libc=:musl)
]

# The products that we will ensure are always built
products(prefix) = [
    ExecutableProduct(prefix, "lotkaVolterraExample", :lotkaVolterraExample),
    LibraryProduct(prefix, "libboost_prg_exec_monitor", Symbol("libboost_prg_exec_monitor.so")),
    LibraryProduct(prefix, "libboost_math_c99l", Symbol("libboost_math_c99l.so")),
    LibraryProduct(prefix, "libboost_math_tr1f", Symbol("libboost_math_tr1f.so")),
    LibraryProduct(prefix, "libboost_math_tr1l", Symbol("libboost_math_tr1l.so")),
    LibraryProduct(prefix, "libboost_math_tr1", Symbol("libboost_math_tr1.so")),
    LibraryProduct(prefix, "libboost_math_c99f", Symbol("libboost_math_c99f.so")),
    LibraryProduct(prefix, "libboost_chrono", Symbol("libboost_chrono.so")),
    ExecutableProduct(prefix, "lotkaVolterraWithType2RegsExample", :lotkaVolterraWithType2RegsExample),
    LibraryProduct(prefix, "libboost_math_c99", Symbol("ibboost_math_c99.so")),
    LibraryProduct(prefix, "libboost_timer", Symbol("libboost_timer.so")),
    LibraryProduct(prefix, "libboost_unit_test_framework", Symbol("libboost_unit_test_framework.so")),
    LibraryProduct(prefix, "libboost_system", Symbol("libboost_system.so")),
    ExecutableProduct(prefix, "ThreeStepMetabolicNetworkExample", :ThreeStepMetabolicNetworkExample)
]

# Dependencies that must be installed before this package can be built
dependencies = [

]

# Build the tarballs, and possibly a `build.jl` as well.
build_tarballs(ARGS, name, version, sources, script, platforms, products, dependencies)


import Aswing
using Test

mydir,_ = splitdir(@__FILE__)

# tolerance used for tests
atol = 1e-5

# Read and input .asw file
inputasw = joinpath(mydir,"hawk.asw")
asw = Aswing.readasw(inputasw)
Aswing.setgeom(asw)

# Construct .pnt file from scratch
constraints = Aswing.Constraints(
    linear_acceleration = [Aswing.IPFORX,Aswing.IPFORY,Aswing.IPFORZ],
    angular_acceleration = [Aswing.IPMOMX,Aswing.IPMOMY,Aswing.IPMOMZ],
    velocity = [Aswing.IPVIAS,Aswing.IPBETA,Aswing.IPVACZ],
    rotation_rate = [Aswing.IPROTX,Aswing.IPROTY,Aswing.IPROTZ],
    position = [Aswing.IPPOSX,Aswing.IPPOSY,Aswing.IPPOSZ],
    phi = Aswing.IPBANK,
    theta = Aswing.IPVACX,
    psi = Aswing.IPHEAD,
    flap_defl_ctrl_var = [Aswing.IPRACX, Aswing.IPRACY, Aswing.IPRACZ, Aswing.IPFLP4],
    eng_pwr_ctrl_var = [Aswing.IPENG1])

parameters = Aswing.Parameters(
    linear_acceleration = zeros(3),
    angular_acceleration = zeros(3),
    velocity = 60.0,
    beta = 0.0,
    alpha = 1.968,
    rotation_rate = zeros(3),
    position = zeros(3),
    phi = 0.0,
    theta = 0.29,
    psi = 0.0,
    flap_defl_ctrl_var = [0.0, -1.8050, 0.0, 0.0],
    eng_pwr_ctrl_var = [0.0],
    sum_force = zeros(3),
    sum_mom = zeros(3),
    lift = 332.158,
    climb_angle = -1.678,
    radial_acceleration = 0.0,
    usr1 = 0.0,
    usr2 = 0.0,
    least_squared = 0.0)

pnt = [Aswing.OperatingPoint(
    constraints = constraints,
    parameters = parameters,
    mach_from_airspeed = false,
    machpg = 0.0)]

Aswing.setpnt(pnt)

# Save .asw and .pnt files
outputasw = joinpath(mydir,"out.asw")
Aswing.aswsav(outputasw)

outputpnt = joinpath(mydir,"out.pnt")
Aswing.pntsav(outputpnt)

# test reading/writing .asw files
newasw = Aswing.readasw(outputasw)
@testset "asw" begin
    @test asw.namein == newasw.namein
    @test asw.si_units == newasw.si_units
    @test asw.constants == newasw.constants
    @testset "reference values" begin
        for field in fieldnames(Aswing.ReferenceValues)
            @test isapprox(getfield(asw.refvals, field), getfield(newasw.refvals, field))
        end
    end
    @testset "weights" begin
        for i = 1:length(asw.weights)
            for field in fieldnames(Aswing.Weight)
                @test isapprox(getfield(asw.weights[i], field), getfield(newasw.weights[i], field))
            end
        end
    end
    @testset "sensors" begin
        for i = 1:length(asw.sensors)
            for field in fieldnames(Aswing.Sensor)
                @test isapprox(getfield(asw.sensors[i], field), getfield(newasw.sensors[i], field))
            end
        end
    end
    @testset "engines" begin
        for i = 1:length(asw.engines)
            for field in fieldnames(Aswing.Engine)
                @test isapprox(getfield(asw.engines[i], field), getfield(newasw.engines[i], field))
            end
        end
    end
    @test asw.struts == newasw.struts
    @test asw.joints == newasw.joints
    @test asw.jangles == newasw.jangles
    @test asw.grounds == newasw.grounds
    @testset "beams" begin
        for i = 1:length(asw.beams)
            asw.beams[i].number == newasw.beams[i].number
            asw.beams[i].physical_index == newasw.beams[i].physical_index
            asw.beams[i].name == newasw.beams[i].name
            for field in fieldnames(Aswing.SpanwiseVariables)
                @test isapprox(getfield(asw.beams[i].spanwise_variables, field),
                    getfield(newasw.beams[i].spanwise_variables, field))
            end
        end
    end
end

# test reading/writing operating points
newpnt = Aswing.readpnt(outputpnt)
@testset "pnt" begin
    for i = 1:length(pnt)
        @testset "constraints" begin
            for field in fieldnames(Aswing.Constraints)
                pntvar = getfield(pnt[i].constraints, field)
                newpntvar = getfield(newpnt[i].constraints, field)
                if field == :flap_defl_ctrl_var
                    @test pntvar[1] == newpntvar[1]
                    @test pntvar[2] == newpntvar[2]
                    @test pntvar[3] == newpntvar[3]
                    @test pntvar[4] == newpntvar[4]
                elseif field == :eng_pwr_ctrl_var
                    @test pntvar[1] == newpntvar[1]
                else
                    @test pntvar == newpntvar
                end
            end
        end
        @testset "parameters" begin
            for field in fieldnames(Aswing.Parameters)
                pntvar = getfield(pnt[i].parameters, field)
                newpntvar = getfield(newpnt[i].parameters, field)
                if field == :flap_defl_ctrl_var
                    @test pntvar[1] == newpntvar[1]
                    @test pntvar[2] == newpntvar[2]
                    @test pntvar[3] == newpntvar[3]
                    @test pntvar[4] == newpntvar[4]
                elseif field == :eng_pwr_ctrl_var
                    @test pntvar[1] == newpntvar[1]
                else
                    @test isapprox(pntvar, newpntvar, atol=1e-2)
                end
            end
        end
        @test pnt[i].mach_from_airspeed == newpnt[i].mach_from_airspeed
        @test pnt[i].machpg == newpnt[i].machpg
        @test pnt[i].ground_image == newpnt[i].ground_image
        @test pnt[i].wflap == newpnt[i].wflap
        @test pnt[i].wpeng == newpnt[i].wpeng
    end
end

# test steady solution
steady_solution = Aswing.solvesteady(1)
@test isapprox(steady_solution.pnt_param.lift_force/steady_solution.pnt_param.drag_force, 33.6092341750539, atol=atol)

# get stability derivatives
stabderivs = Aswing.stabilityderivatives()
@testset "stabderivs" begin
    @test isapprox(stabderivs.CXu, -0.03652047310144528, atol=atol)
    @test isapprox(stabderivs.CYu, 0.0, atol=atol)
    @test isapprox(stabderivs.CLu, 1.216904723395558, atol=atol)
    @test isapprox(stabderivs.CXa, 0.514992210591996, atol=atol)
    @test isapprox(stabderivs.CYa, 0.0, atol=atol)
    @test isapprox(stabderivs.CLa, 6.2730761556092975, atol=atol)
    @test isapprox(stabderivs.CXb, 0.0, atol=atol)
    @test isapprox(stabderivs.CYb, -0.42126215334348716, atol=atol)
    @test isapprox(stabderivs.CLb, 0.0, atol=atol)
    @test isapprox(stabderivs.Clu, 0.0, atol=atol)
    @test isapprox(stabderivs.Cmu, 0.04767457583546996, atol=atol)
    @test isapprox(stabderivs.Cnu, 0.0, atol=atol)
    @test isapprox(stabderivs.Cla, 0.0, atol=atol)
    @test isapprox(stabderivs.Cma, -1.166473545760255, atol=atol)
    @test isapprox(stabderivs.Cna, 0.0, atol=atol)
    @test isapprox(stabderivs.Clb, -0.17672244009048493, atol=atol)
    @test isapprox(stabderivs.Cmb, 0.0, atol=atol)
    @test isapprox(stabderivs.Cnb, 0.033382702016544044, atol=atol)
    @test isapprox(stabderivs.CXp, 0.0, atol=atol)
    @test isapprox(stabderivs.CYp, -0.6745224799824928, atol=atol)
    @test isapprox(stabderivs.CLp, 0.0, atol=atol)
    @test isapprox(stabderivs.CXq, -0.45680201241386725, atol=atol)
    @test isapprox(stabderivs.CYq, 0.0, atol=atol)
    @test isapprox(stabderivs.CLq, 7.284291585430452, atol=atol)
    @test isapprox(stabderivs.CXr, 0.0, atol=atol)
    @test isapprox(stabderivs.CYr, 0.25951346743318265, atol=atol)
    @test isapprox(stabderivs.CLr, 0.0, atol=atol)
    @test isapprox(stabderivs.Clp, -0.7078626211816347, atol=atol)
    @test isapprox(stabderivs.Cmp, 0.0, atol=atol)
    @test isapprox(stabderivs.Cnp, -0.06251261778613514, atol=atol)
    @test isapprox(stabderivs.Clq, 0.0, atol=atol)
    @test isapprox(stabderivs.Cmq, -16.26577215749712, atol=atol)
    @test isapprox(stabderivs.Cnq, 0.0, atol=atol)
    @test isapprox(stabderivs.Clr, 0.1623276548641932, atol=atol)
    @test isapprox(stabderivs.Cmr, 0.0, atol=atol)
    @test isapprox(stabderivs.Cnr, -0.03741744012260867, atol=atol)
    @test isapprox(stabderivs.CXdF, [0.0, 5.16115826001186e-6, 0.0, -0.0010600668088926565], atol=atol)
    @test isapprox(stabderivs.CYdF, [0.0065501671250972815, 0.0, 0.0019375002936612249, 0.0], atol=atol)
    @test isapprox(stabderivs.CLdF, [0.0, 0.0037351659479835007, 0.0, 0.0490397612326285], atol=atol)
    @test isapprox(stabderivs.CXdP, [0.0018661778027707714], atol=atol)
    @test isapprox(stabderivs.CYdP, [-5.292539460008798e-9], atol=atol)
    @test isapprox(stabderivs.CLdP, [-0.00010449565952022606], atol=atol)
end

# test setting new paneling
newintervals = [Aswing.getintervals(i) for i  in 1:4]*2
Aswing.nodeset(newintervals)
@test all(newintervals .== [Aswing.getintervals(i) for i in 1:4])

# make sure solution remains similar
steady_solution = Aswing.solvesteady(1)
@test isapprox(steady_solution.pnt_param.lift_force/steady_solution.pnt_param.drag_force, 33.6573231422232, atol=atol)

# get static margin (note that this is using the increased paneling)
sm_qs, sm_rigid, fail = Aswing.getstaticmargin(1)
@test isapprox(sm_qs/100, 0.26230193526999124, atol=atol)
@test isapprox(sm_rigid/100, 0.31478364312678236, atol=atol)

# recover from static margin test (restores default paneling)
Aswing.setgeom(asw)
Aswing.setpnt(pnt)
steady_solution = Aswing.solvesteady(1)

# set up and test unsteady solution
Aswing.retainparam()
Aswing.setcons(Aswing.Constraints("free"))
unsteady_solution = Aswing.solveunsteady(0.1, 10)
@test isapprox(unsteady_solution[11].pnt_param.lift_force/unsteady_solution[11].pnt_param.drag_force, 33.6092341750539, atol=atol)

# test eigenvalues
asweigs = Aswing.geteigs(16, -0.1+0.0im, 1)
@test isapprox(asweigs,
[
-0.02665193416160748 + 0.0im               ,
-0.01689216581284332 + 0.4242720980364713im,
 -0.5286716816061375 + 1.521907733573002im ,
  -7.053040642626703 + 0.0im               ,
 -3.8200059691829984 + 6.2769874595045065im,
  -8.726782350618162 + 8.588264563476661im ,
 -11.749382348992544 + 7.338095974651897im,
 ], atol=atol)


module TestOperators

using JuMP
using LinearAlgebra
using SparseArrays
using Test

const MA = JuMP._MA

include(joinpath(@__DIR__, "utilities.jl"))
include(joinpath(@__DIR__, "JuMPExtension.jl"))

# For "DimensionMismatch when performing vector-matrix multiplication with custom types #988"
import Base: +, *
struct MyType{T}
    a::T
end
struct MySumType{T}
    a::T
end
Base.copy(t::MyType) = t
Base.zero(::Type{MyType{T}}) where {T} = MyType(zero(T))
Base.one(::Type{MyType{T}}) where {T} = MyType(one(T))
Base.zero(::Type{MySumType{T}}) where {T} = MySumType(zero(T))
Base.zero(::MySumType{T}) where {T} = MySumType(zero(T))
Base.transpose(t::MyType) = MyType(t.a)
Base.transpose(t::MySumType) = MySumType(t.a)
LinearAlgebra.adjoint(t::Union{MyType,MySumType}) = t
function +(
    t1::MyT,
    t2::MyS,
) where {MyT<:Union{MyType,MySumType},MyS<:Union{MyType,MySumType}}
    return MySumType(t1.a + t2.a)
end
*(t1::MyType{S}, t2::T) where {S,T} = MyType(t1.a * t2)
*(t1::S, t2::MyType{T}) where {S,T} = MyType(t1 * t2.a)
*(t1::MyType{S}, t2::MyType{T}) where {S,T} = MyType(t1.a * t2.a)

function JuMP.isequal_canonical(t::MySumType, s::MySumType)
    return JuMP.isequal_canonical(t.a, s.a)
end
function JuMP.isequal_canonical(
    x::AbstractArray{<:MySumType},
    y::AbstractArray{<:MySumType},
)
    return size(x) == size(y) && all(JuMP.isequal_canonical.(x, y))
end

function test_promotion(ModelType, VariableRefType)
    m = ModelType()
    I = Int
    V = VariableRefType
    A = JuMP.GenericAffExpr{Float64,VariableRefType}
    Q = JuMP.GenericQuadExpr{Float64,VariableRefType}
    @test promote_type(V, I) == A
    @test promote_type(I, V) == A
    @test promote_type(A, I) == A
    @test promote_type(I, A) == A
    @test promote_type(A, V) == A
    @test promote_type(V, A) == A
    @test promote_type(Q, I) == Q
    @test promote_type(I, Q) == Q
    @test promote_type(Q, A) == Q
    @test promote_type(A, Q) == Q
    @test promote_type(Q, V) == Q
    @test promote_type(V, Q) == Q
end

function test_broadcast_division_error(ModelType, ::Any)
    model = ModelType()
    @variable(model, x[1:2, 1:2])
    A = [1 2; 3 4]
    B = sparse(A)
    y = SparseMatrixCSC(2, 2, copy(B.colptr), copy(B.rowval), vec(x))
    @test_throws ErrorException A ./ x
    @test_throws ErrorException B ./ x
    @test_throws ErrorException A ./ y
    @test_throws ErrorException B ./ y

    # TODO: Refactor to avoid calling the internal JuMP function
    # `_densify_with_jump_eltype`.
    #z = JuMP._densify_with_jump_eltype((2 .* y) ./ 3)
    #@test JuMP.isequal_canonical((2 .* x) ./ 3, z)
    #z = JuMP._densify_with_jump_eltype(2 * (y ./ 3))
    #@test JuMP.isequal_canonical(2 .* (x ./ 3), z)
    #z = JuMP._densify_with_jump_eltype((x[1,1],) .* B)
    #@test JuMP.isequal_canonical((x[1,1],) .* A, z)
end

function test_vectorized_comparisons(ModelType, ::Any)
    m = ModelType()
    @variable(m, x[1:3])
    A = [
        1 2 3
        0 4 5
        6 0 7
    ]
    B = sparse(A)
    # force vector output
    cref1 = @constraint(m, reshape(x, (1, 3)) * A * x .>= 1)
    c1 = JuMP.constraint_object.(cref1)
    f1 = map(c -> c.func, c1)
    @test JuMP.isequal_canonical(
        f1,
        [
            x[1] * x[1] +
            2x[1] * x[2] +
            4x[2] * x[2] +
            9x[1] * x[3] +
            5x[2] * x[3] +
            7x[3] * x[3],
        ],
    )
    @test all(c -> c.set.lower == 1, c1)

    cref2 = @constraint(m, x' * A * x >= 1)
    c2 = JuMP.constraint_object.(cref2)
    @test JuMP.isequal_canonical(f1[1], c2.func)

    mat = [
        3x[1] + 12x[3] + 4x[2]
        2x[1] + 12x[2] + 10x[3]
        15x[1] + 5x[2] + 21x[3]
    ]

    cref3 = @constraint(m, (x'A)' + 2A * x .<= 1)
    c3 = JuMP.constraint_object.(cref3)
    f3 = map(c -> c.func, c3)
    @test JuMP.isequal_canonical(f3, mat)
    @test all(c -> c.set.upper == 1, c3)
    @test JuMP.isequal_canonical((x'A)' + 2A * x, (x'A)' + 2B * x)
    @test JuMP.isequal_canonical((x'A)' + 2A * x, (x'B)' + 2A * x)
    @test JuMP.isequal_canonical((x'A)' + 2A * x, (x'B)' + 2B * x)
    @test JuMP.isequal_canonical((x'A)' + 2A * x, MA.@rewrite((x'A)' + 2A * x))
    @test JuMP.isequal_canonical((x'A)' + 2A * x, MA.@rewrite((x'B)' + 2A * x))
    @test JuMP.isequal_canonical((x'A)' + 2A * x, MA.@rewrite((x'A)' + 2B * x))
    @test JuMP.isequal_canonical((x'A)' + 2A * x, MA.@rewrite((x'B)' + 2B * x))

    cref4 = @constraint(m, -1 .<= (x'A)' + 2A * x .<= 1)
    c4 = JuMP.constraint_object.(cref4)
    f4 = map(c -> c.func, c4)
    @test JuMP.isequal_canonical(f4, mat)
    @test all(c -> c.set.lower == -1, c4)
    @test all(c -> c.set.upper == 1, c4)

    cref5 = @constraint(m, -[1:3;] .<= (x'A)' + 2A * x .<= 1)
    c5 = JuMP.constraint_object.(cref5)
    f5 = map(c -> c.func, c5)
    @test JuMP.isequal_canonical(f5, mat)
    @test map(c -> c.set.lower, c5) == -[1:3;]
    @test all(c -> c.set.upper == 1, c4)

    cref6 = @constraint(m, -[1:3;] .<= (x'A)' + 2A * x .<= [3:-1:1;])
    c6 = JuMP.constraint_object.(cref6)
    f6 = map(c -> c.func, c6)
    @test JuMP.isequal_canonical(f6, mat)
    @test map(c -> c.set.lower, c6) == -[1:3;]
    @test map(c -> c.set.upper, c6) == [3:-1:1;]

    cref7 = @constraint(m, -[1:3;] .<= (x'A)' + 2A * x .<= 3)
    c7 = JuMP.constraint_object.(cref7)
    f7 = map(c -> c.func, c7)
    @test JuMP.isequal_canonical(f7, mat)
    @test map(c -> c.set.lower, c7) == -[1:3;]
    @test all(c -> c.set.upper == 3, c7)
end

function test_custom_dimension_mismatch(ModelType, VariableRefType)
    ElemT = MySumType{JuMP.GenericAffExpr{Float64,VariableRefType}}
    model = ModelType()
    @variable(model, Q[1:3, 1:3], PSD)
    x = [MyType(1), MyType(2), MyType(3)]
    y = Q * x
    z = x' * Q
    @test y isa Vector{ElemT}
    @test size(y) == (3,)
    @test z isa Adjoint{ElemT,<:Any}
    @test size(z) == (1, 3)
    for i in 1:3
        # Q is symmetric
        a = zero(JuMP.GenericAffExpr{Float64,VariableRefType})
        a += Q[1, i]
        a += 2Q[2, i]
        a += 3Q[3, i]
        # Q[1,i] + 2Q[2,i] + 3Q[3,i] is rearranged as 2 Q[2,3] + Q[1,3] + 3 Q[3,3]
        @test z[i].a == y[i].a == a
    end
end

function test_matrix_multiplication(ModelType, VariableRefType)
    ElemT = MySumType{JuMP.GenericAffExpr{Float64,VariableRefType}}
    model = ModelType()
    @variable(model, Q[1:3, 1:3], PSD)
    X = MyType.((1:3)' .+ (1:3))
    @test_expression Q * X
    Y = Q * X
    @test_expression X' * Q
    Z = X' * Q
    @test Y isa Matrix{ElemT}
    @test size(Y) == (3, 3)
    @test Z isa Matrix{ElemT}
    @test size(Z) == (3, 3)
    @test JuMP.isequal_canonical(Z', Y)
    @test_expression Q * X'
    Y = Q * X'
    @test_expression X * Q
    Z = X * Q
    @test Y isa Matrix{ElemT}
    @test size(Y) == (3, 3)
    @test Z isa Matrix{ElemT}
    @test size(Z) == (3, 3)
    @test JuMP.isequal_canonical(Z', Y)
end

function test_operator_warn(ModelType, ::Any)
    model = ModelType()
    @variable model x[1:51]
    # JuMPExtension does not have the `operator_counter` field
    if ModelType <: Model
        @test model.operator_counter == 0
    end
    # Triggers the increment of operator_counter since sum(x) has more than 50 terms
    @test_expression(sum(x) + 2x[1])
    if ModelType <: Model
        # The following check verifies that this test covers the code incrementing `operator_counter`
        @test model.operator_counter == 1
    end
end

function test_uniform_scaling(ModelType, ::Any)
    model = ModelType()
    @variable(model, x)
    @test_expression_with_string x + 2I "x + 2"
    @test_expression_with_string (x + 1) + I "x + 2"
    @test_expression_with_string x - 2I "x - 2"
    @test_expression_with_string (x - 1) - I "x - 2"
    @test_expression_with_string 2I + x "x + 2"
    @test_expression_with_string I + (x + 1) "x + 2"
    @test_expression_with_string 2I - x "-x + 2"
    @test_expression_with_string I - (x - 1) "-x + 2"
    @test_expression_with_string I * x "x"
    @test_expression_with_string I * (x + 1) "x + 1"
    @test_expression_with_string (x + 1) * I "x + 1"
end

function test_basic_operators(ModelType, ::Any)
    model = ModelType()
    @variable(model, w)
    @variable(model, x)
    @variable(model, y)
    @variable(model, z)

    aff = @inferred 7.1 * x + 2.5
    @test_expression_with_string 7.1 * x + 2.5 "7.1 x + 2.5"
    aff2 = @inferred 1.2 * y + 1.2
    @test_expression_with_string 1.2 * y + 1.2 "1.2 y + 1.2"
    q = @inferred 2.5 * y * z + aff
    @test_expression_with_string 2.5 * y * z + aff "2.5 y*z + 7.1 x + 2.5"
    q2 = @inferred 8 * x * z + aff2
    @test_expression_with_string 8 * x * z + aff2 "8 x*z + 1.2 y + 1.2"
    @test_expression_with_string 2 * x * x + 1 * y * y + z + 3 "2 x² + y² + z + 3"
end

function test_basic_operators_number(ModelType, ::Any)
    model = ModelType()
    @variable(model, w)
    @variable(model, x)
    @variable(model, y)
    @variable(model, z)

    aff = @inferred 7.1 * x + 2.5
    q = @inferred 2.5 * y * z + aff

    # 1-1 Number--Number
    # ???
    # 1-2 Number--Variable
    @test_expression_with_string 4.13 + w "w + 4.13"
    @test_expression_with_string 3.16 - w "-w + 3.16"
    @test_expression_with_string 5.23 * w "5.23 w"
    @test_throws ErrorException 2.94 / w
    # 1-3 Number--AffExpr
    @test_expression_with_string 1.5 + aff "7.1 x + 4"
    @test_expression_with_string 1.5 - aff "-7.1 x - 1"
    @test_expression_with_string 2 * aff "14.2 x + 5"
    @test_throws ErrorException 2 / aff
    # 1-4 Number--QuadExpr
    @test_expression_with_string 1.5 + q "2.5 y*z + 7.1 x + 4"
    @test_expression_with_string 1.5 - q "-2.5 y*z - 7.1 x - 1"
    @test_expression_with_string 2 * q "5 y*z + 14.2 x + 5"
    @test_throws ErrorException 2 / q
end

function test_basic_operators_variable(ModelType, ::Any)
    model = ModelType()
    @variable(model, w)
    @variable(model, x)
    @variable(model, y)
    @variable(model, z)

    aff = @inferred 7.1 * x + 2.5
    q = @inferred 2.5 * y * z + aff

    # 2-0 Variable unary
    @test (+x) === x
    @test_expression_with_string -x "-x"
    # 2-1 Variable--Number
    @test_expression_with_string w + 4.13 "w + 4.13"
    @test_expression_with_string w - 4.13 "w - 4.13"
    @test_expression_with_string w * 4.13 "4.13 w"
    @test_expression_with_string w / 2.00 "0.5 w"
    @test w == w
    @test_expression_with_string x * y - 1 "x*y - 1"
    @test_expression_with_string x^2 "x²"
    @test_expression_with_string x^1 "x"
    @test_expression_with_string x^0 "1"
    @test_throws ErrorException x^3
    @test_throws ErrorException x^1.5
    # 2-2 Variable--Variable
    @test_expression_with_string w + x "w + x"
    @test_expression_with_string w - x "w - x"
    @test_expression_with_string w * x "w*x"
    @test_expression_with_string x - x "0"
    @test_throws ErrorException w / x
    @test_expression_with_string y * z - x "y*z - x"
    # 2-3 Variable--AffExpr
    @test_expression_with_string z + aff "z + 7.1 x + 2.5"
    @test_expression_with_string z - aff "z - 7.1 x - 2.5"
    @test_expression_with_string z * aff "7.1 z*x + 2.5 z"
    @test_throws ErrorException z / aff
    @test_throws MethodError z ≤ aff
    @test_expression_with_string 7.1 * x - aff "0 x - 2.5"
    # 2-4 Variable--QuadExpr
    @test_expression_with_string w + q "2.5 y*z + w + 7.1 x + 2.5"
    @test_expression_with_string w - q "-2.5 y*z + w - 7.1 x - 2.5"
    @test_throws ErrorException w * q
    @test_throws ErrorException w / q
    @test transpose(x) === x
    @test conj(x) === x
end

function test_basic_operators_affexpr(ModelType, ::Any)
    model = ModelType()
    @variable(model, w)
    @variable(model, x)
    @variable(model, y)
    @variable(model, z)

    aff = @inferred 7.1 * x + 2.5
    aff2 = @inferred 1.2 * y + 1.2
    q = @inferred 2.5 * y * z + aff
    # 3-0 AffExpr unary
    @test_expression_with_string +aff "7.1 x + 2.5"
    @test_expression_with_string -aff "-7.1 x - 2.5"
    # 3-1 AffExpr--Number
    @test_expression_with_string aff + 1.5 "7.1 x + 4"
    @test_expression_with_string aff - 1.5 "7.1 x + 1"
    @test_expression_with_string aff * 2 "14.2 x + 5"
    @test_expression_with_string aff / 2 "3.55 x + 1.25"
    @test_throws MethodError aff ≤ 1
    @test aff == aff
    @test_throws MethodError aff ≥ 1
    @test_expression_with_string aff - 1 "7.1 x + 1.5"
    @test_expression_with_string aff^2 "50.41 x² + 35.5 x + 6.25"
    @test_expression_with_string (7.1 * x + 2.5)^2 "50.41 x² + 35.5 x + 6.25"
    @test_expression_with_string aff^1 "7.1 x + 2.5"
    @test_expression_with_string (7.1 * x + 2.5)^1 "7.1 x + 2.5"
    @test_expression_with_string aff^0 "1"
    @test_expression_with_string (7.1 * x + 2.5)^0 "1"
    @test_throws ErrorException aff^3
    @test_throws ErrorException (7.1 * x + 2.5)^3
    @test_throws ErrorException aff^1.5
    @test_throws ErrorException (7.1 * x + 2.5)^1.5
    # 3-2 AffExpr--Variable
    @test_expression_with_string aff + z "7.1 x + z + 2.5"
    @test_expression_with_string aff - z "7.1 x - z + 2.5"
    @test_expression_with_string aff * z "7.1 x*z + 2.5 z"
    @test_throws ErrorException aff / z
    @test_expression_with_string aff - 7.1 * x "0 x + 2.5"
    # 3-3 AffExpr--AffExpr
    @test_expression_with_string aff + aff2 "7.1 x + 1.2 y + 3.7"
    @test_expression_with_string aff - aff2 "7.1 x - 1.2 y + 1.3"
    @test_expression_with_string aff * aff2 "8.52 x*y + 3 y + 8.52 x + 3"
    @test string((x + x) * (x + 3)) == string((x + 3) * (x + x))  # Issue #288
    @test_throws ErrorException aff / aff2
    @test_expression_with_string aff - aff "0 x"
    # 4-4 AffExpr--QuadExpr
    @test_expression_with_string aff2 + q "2.5 y*z + 1.2 y + 7.1 x + 3.7"
    @test_expression_with_string aff2 - q "-2.5 y*z + 1.2 y - 7.1 x - 1.3"
    @test_throws ErrorException aff2 * q
    @test_throws ErrorException aff2 / q
    @test transpose(aff) === aff
    @test conj(aff) === aff
end

function test_basic_operators_quadexpr(ModelType, ::Any)
    model = ModelType()
    @variable(model, w)
    @variable(model, x)
    @variable(model, y)
    @variable(model, z)

    aff = @inferred 7.1 * x + 2.5
    aff2 = @inferred 1.2 * y + 1.2
    q = @inferred 2.5 * y * z + aff
    q2 = @inferred 8 * x * z + aff2
    # 4-0 QuadExpr unary
    @test_expression_with_string +q "2.5 y*z + 7.1 x + 2.5"
    @test_expression_with_string -q "-2.5 y*z - 7.1 x - 2.5"
    # 4-1 QuadExpr--Number
    @test_expression_with_string q + 1.5 "2.5 y*z + 7.1 x + 4"
    @test_expression_with_string q - 1.5 "2.5 y*z + 7.1 x + 1"
    @test_expression_with_string q * 2 "5 y*z + 14.2 x + 5"
    @test_expression_with_string q / 2 "1.25 y*z + 3.55 x + 1.25"
    @test q == q
    @test_expression_with_string aff2 - q "-2.5 y*z + 1.2 y - 7.1 x - 1.3"
    # 4-2 QuadExpr--Variable
    @test_expression_with_string q + w "2.5 y*z + 7.1 x + w + 2.5"
    @test_expression_with_string q - w "2.5 y*z + 7.1 x - w + 2.5"
    @test_throws ErrorException q * w
    @test_throws ErrorException q / w
    # 4-3 QuadExpr--AffExpr
    @test_expression_with_string q + aff2 "2.5 y*z + 7.1 x + 1.2 y + 3.7"
    @test_expression_with_string q - aff2 "2.5 y*z + 7.1 x - 1.2 y + 1.3"
    @test_throws ErrorException q * aff2
    @test_throws ErrorException q / aff2
    # 4-4 QuadExpr--QuadExpr
    @test_expression_with_string q + q2 "2.5 y*z + 8 x*z + 7.1 x + 1.2 y + 3.7"
    @test_expression_with_string q - q2 "2.5 y*z - 8 x*z + 7.1 x - 1.2 y + 1.3"
    @test_throws ErrorException q * q2
    @test_throws ErrorException q / q2
    @test transpose(q) === q
    @test conj(q) === q
end

function test_dot(ModelType, ::Any)
    model = ModelType()
    @variable(model, 0 ≤ x[1:3] ≤ 1)

    @test_expression_with_string dot(x[1], x[1]) "x[1]²"
    @test_expression_with_string dot(2, x[1]) "2 x[1]"
    @test_expression_with_string dot(x[1], 2) "2 x[1]"

    c = vcat(1:3)
    @test_expression_with_string dot(c, x) "x[1] + 2 x[2] + 3 x[3]"
    @test_expression_with_string dot(x, c) "x[1] + 2 x[2] + 3 x[3]"

    A = [1 3; 2 4]
    @variable(model, 1 ≤ y[1:2, 1:2] ≤ 1)
    @test_expression_with_string dot(A, y) "y[1,1] + 2 y[2,1] + 3 y[1,2] + 4 y[2,2]"
    @test_expression_with_string dot(y, A) "y[1,1] + 2 y[2,1] + 3 y[1,2] + 4 y[2,2]"

    B = ones(2, 2, 2)
    @variable(model, 0 ≤ z[1:2, 1:2, 1:2] ≤ 1)
    @test_expression_with_string dot(B, z) "z[1,1,1] + z[2,1,1] + z[1,2,1] + z[2,2,1] + z[1,1,2] + z[2,1,2] + z[1,2,2] + z[2,2,2]"
    @test_expression_with_string dot(z, B) "z[1,1,1] + z[2,1,1] + z[1,2,1] + z[2,2,1] + z[1,1,2] + z[2,1,2] + z[1,2,2] + z[2,2,2]"

    @objective(model, Max, dot(x, ones(3)) - dot(y, ones(2, 2)))
    for i in 1:3
        JuMP.set_start_value(x[i], 1)
    end
    for i in 1:2, j in 1:2
        JuMP.set_start_value(y[i, j], 1)
    end
    for i in 1:2, j in 1:2, k in 1:2
        JuMP.set_start_value(z[i, j, k], 1)
    end
    @test dot(c, JuMP.start_value.(x)) ≈ 6
    @test dot(A, JuMP.start_value.(y)) ≈ 10
    @test dot(B, JuMP.start_value.(z)) ≈ 8

    if ModelType <: Model
        # Only `Model` is guaranteed to have `operator_counter`, so
        # only test for that case.
        @testset "dot doesn't trigger operator_counter" begin
            # Check that dot is not falling back to default, inefficient
            # addition (JuMP PR #943).
            model = ModelType()
            @test model.operator_counter == 0
            @variable(model, x[1:100])
            JuMP.set_start_value.(x, 1:100)
            @expression(model, test_sum, sum(x[i] * i for i in 1:100))
            @expression(model, test_dot1, dot(x, 1:100))
            @expression(model, test_dot2, dot(1:100, x))
            @test model.operator_counter == 0
            test_add = test_dot1 + test_dot2
            @test model.operator_counter == 1  # Check triggerable.
            test_sum_value = JuMP.value(test_sum, JuMP.start_value)
            @test test_sum_value ≈ JuMP.value(test_dot1, JuMP.start_value)
            @test test_sum_value ≈ JuMP.value(test_dot2, JuMP.start_value)
        end
    end
end

function test_higher_level(ModelType, ::Any)
    model = ModelType()
    @variable(model, 0 ≤ matrix[1:3, 1:3] ≤ 1, start = 1)
    @testset "sum(j::DenseAxisArray{Variable})" begin
        @test_expression_with_string sum(matrix) "matrix[1,1] + matrix[2,1] + matrix[3,1] + matrix[1,2] + matrix[2,2] + matrix[3,2] + matrix[1,3] + matrix[2,3] + matrix[3,3]"
    end

    @testset "sum(j::DenseAxisArray{T}) where T<:Real" begin
        @test sum(JuMP.start_value.(matrix)) ≈ 9
    end
    @testset "sum(j::Array{VariableRef})" begin
        @test string(sum(matrix[1:3, 1:3])) == string(sum(matrix))
    end
    @testset "sum(affs::Array{AffExpr})" begin
        @test_expression_with_string sum([
            2 * matrix[i, j] for i in 1:3, j in 1:3
        ]) "2 matrix[1,1] + 2 matrix[2,1] + 2 matrix[3,1] + 2 matrix[1,2] + 2 matrix[2,2] + 2 matrix[3,2] + 2 matrix[1,3] + 2 matrix[2,3] + 2 matrix[3,3]"
    end
    @testset "sum(quads::Array{QuadExpr})" begin
        @test_expression_with_string sum([
            2 * matrix[i, j]^2 for i in 1:3, j in 1:3
        ]) "2 matrix[1,1]² + 2 matrix[2,1]² + 2 matrix[3,1]² + 2 matrix[1,2]² + 2 matrix[2,2]² + 2 matrix[3,2]² + 2 matrix[1,3]² + 2 matrix[2,3]² + 2 matrix[3,3]²"
    end
    S = [1, 3]
    @variable(model, x[S], start = 1)
    @testset "sum(j::JuMPDict{VariableRef})" begin
        @test_expression sum(x)
        @test length(string(sum(x))) == 11 # order depends on hashing
        @test occursin("x[1]", string(sum(x)))
        @test occursin("x[3]", string(sum(x)))
    end
    @testset "sum(j::JuMPDict{T}) where T<:Real" begin
        @test sum(JuMP.start_value.(x)) == 2
    end
end

function runtests()
    for name in names(@__MODULE__; all = true)
        if !startswith("$(name)", "test_")
            continue
        end
        f = getfield(@__MODULE__, name)
        @testset "$(name)" begin
            f(Model, VariableRef)
        end
        @testset "$(name)-JuMPExtension" begin
            f(JuMPExtension.MyModel, JuMPExtension.MyVariableRef)
        end
    end
end

end

TestOperators.runtests()


#
# This file is part of module
#   ModiaMedia (ModiaMedia/src/ModiaMedia.jl)
#

#=
# A medium is defined by a struct and accompanying functions. The struct has the following structure:
    struct MediumXXX <: AbstractMedium
        mediumName::AbstractString
        substanceNames::Vector{AbstractString}
        < ... other generic data >
        fluidConstants::Vector{AbstractFluidConstants}
        fluidLimits::FluidLimits
        data  # medium specific data
    end

    All media are stored in dictionary mediumDict
=#

undefinedFunction(name::AbstractString, m::AbstractMedium) =
    error("\n\nModiaMedia: Function $name not defined for medium $(m.infos.mediumName).\n")

### Enumerations and constants --------------------------------------------------------------------

"""
    @enum IndependentVariables

Enumeration defining the independent variables of a medium. Possible values:

| Value                      | Independent variables                       |
|:---------------------------|:--------------------------------------------|
| `IndependentVariables_T`   | Temperature                                 |
| `IndependentVariables_pT`  | Pressure, temperature                       |
| `IndependentVariables_ph`  | Pressure, specific enthalpy                 |
| `IndependentVariables_phX` | Pressure, specific enthalpy, mass fractions |
| `IndependentVariables_pTX` | Pressure, temperature, mass fractions       |
| `IndependentVariables_dTX` | Density, temperature, mass fractions        |
"""
@enum IndependentVariables begin
    IndependentVariables_T=1       # Temperature
    IndependentVariables_pT        # Pressure, Temperature
    IndependentVariables_ph        # Pressure, Specific Enthalpy
    IndependentVariables_phX       # Pressure, Specific Enthalpy, Mass Fraction
    IndependentVariables_pTX       # Pressure, Temperature, Mass Fractions
    IndependentVariables_dTX       # Density, Temperature, Mass Fractions
end

"""
    state = ThermodynamicState_pT(p,T)

Generate a `ThermodynamicState_pT <: ThermodynamicState` object containg
pressure `p` [Pa] and temperature `T` [K] as states.
"""
mutable struct ThermodynamicState_pT <: ThermodynamicState
    p::Float64
    T::Float64
end

const ThermodynamicStates = MVector{6,Any}(nothing,
                                           ThermodynamicState_pT,
                                           nothing,
                                           nothing,
                                           nothing,
                                           nothing)

"""
    @enum ReferenceEnthalpy

Enumeration defining the reference enthalpy of a medium. Possible values:

- `ReferenceEnthalpy_ZeroAt0K`: The enthalpy is 0 at 0 K (default), if the enthalpy of formation is excluded
- `ReferenceEnthalpy_ZeroAt25C`: The enthalpy is 0 at 25 degC, if the enthalpy of formation is excluded
- `ReferenceEnthalpy_UserDefined`: The user-defined reference enthalpy is used at 293.15 K (25 degC)
"""
@enum ReferenceEnthalpy begin
    ReferenceEnthalpy_ZeroAt0K     # The enthalpy is 0 at 0 K (default), if the enthalpy of formation is excluded
    ReferenceEnthalpy_ZeroAt25C    # The enthalpy is 0 at 25 degC, if the enthalpy of formation is excluded
    ReferenceEnthalpy_UserDefined  # The user-defined reference enthalpy is used at 293.15 K (25 degC)
end

"""
    @enum ReferenceEntropy

Enumeration defining the reference entropy of a medium. Possible values:

- `ReferenceEntropy_ZeroAt0K`: The entropy is 0 at 0 K (default)
- `ReferenceEntropy_ZeroAt0C`: The entropy is 0 at 0 degC
- `ReferenceEntropy_UserDefined`: The user-defined reference entropy is used at 293.15 K (25 degC)
"""
@enum ReferenceEntropy begin
    ReferenceEntropy_ZeroAt0K      # The entropy is 0 at 0 K (default)
    ReferenceEntropy_ZeroAt0C      # The entropy is 0 at 0 degC
    ReferenceEntropy_UserDefined   # The user-defined reference entropy is used at 293.15 K (25 degC)
end

"Enumeration Init defining initialization for fluid flow"
@enum Init begin
    Init_NoInit                    # NoInit (no initialization)
    Init_InitialStates             # InitialStates (initialize medium states)
    Init_SteadyState               # SteadyState (initialize in steady state)
    Init_SteadyMass                # SteadyMass (initialize density or pressure in steady state)
end

"Enumeration pd defining whether p or d are known for the boundary condition"
@enum pd begin
    pd_default                     # Default (no boundary condition for p or d)
    pd_p_known                     # p_known (pressure p is known)
    pd_d_known                     # d_known (density d is known)
end

"Enumeration Th - defining whether T or h are known as boundary condition"
@enum Th begin
    Th_default                     # Default (no boundary condition for T or h)
    Th_T_known                     # T_known (temperature T is known)
    Th_h_known                     # h_known (specific enthalpy h is known)
end

### Data structures ---------------------------------------------------------------------------------

"""
    infos = FluidInfos(; <keyword arguments, see below>)

Generate a new `FluidInfos` object, containing generic properties of a medium.

# Keyword arguments

| Name and type                                  | Description                                                                                |
|:-----------------------------------------------|:--------------------------------------------------------------------------------------------------------|
| `mediumName::AbstractString`                   | Name of the medium                                                                                |
| `substanceNames::Vector{AbstractString}`       | Names of the mixture substances. Set substanceNames=[mediumName] if only one substance.                 |
| `extraPropertiesNames::Vector{AbstractString}` | Names of the additional (extra) transported properties. Set extraPropertiesNames=fill(\"\",0) if unused |
| `ThermoStates::IndependentVariables`           | Enumeration type for independent variables                                                              |
| `baseProperties::Symbol`                       | Symbol of baseProperties model = :BaseProperties_<StructName>                                           |
| `singleState::Bool`                            | = true, if u and d are not a function of pressure                                                       |
| `reducedX::Bool`                               | = true if medium contains the equation sum(X) = 1.0; set reducedX=true if only one substance            |
| `fixedX::Bool`                                 | = true if medium contains the equation X = reference_X                                                  |
| `reference_p`                                  | Reference pressure of Medium: default 1 atmosphere                                                      |
| `reference_T`                                  | Reference temperature of Medium: default 25 deg Celsius                                                 |
| `reference_X`                                  | Reference mass fractions of medium                                                                      |
| `p_default`                                    | Default value for pressure of medium (for initialization)                                               |
| `T_default`                                    | Default value for temperature of medium (for initialization)                                            |
| `h_default`                                    | Default value for specific enthalpy of medium (for initialization)                                      |
| `X_default`                                    | Default value for specific enthalpy of medium (for initialization)                                      |
| `nS`                                           | Number of substances                                                                                |
| `nX`                                           | Number of mass fractions                                                                                |
| `nXi`                                          | Number of structurally independent mass fractions                                                       |
| `nC`                                           | Number of extra (outside of standard mass-balance) transported properties                               |
| `C_nominal`                                    | Default for the nominal values for the extra properties                                                 |

# Example
```julia
import ModiaMedia

infos = ModiaMedia.FluidInfos(mediumName           = "simpleMedium",
                              substanceNames       = [mediumName],
                              extraPropertiesNames = fill("",0),
                              ThermoStates         = IndependentVariables_T)
```
"""
struct FluidInfos
    mediumName::AbstractString                   # "Name of the medium";
    substanceNames::Vector{AbstractString}       # "Names of the mixture substances. Set substanceNames=[mediumName] if only one substance.";
    extraPropertiesNames::Vector{AbstractString} # "Names of the additional (extra) transported properties. Set extraPropertiesNames=fill(\"\",0) if unused"
    ThermoStates::IndependentVariables           # "Enumeration type for independent variables";
    baseProperties::Symbol                       # "Symbol of baseProperties model = :BaseProperties_<StructName>
    singleState::Bool                            # "= true, if u and d are not a function of pressure";
    reducedX::Bool                               # "= true if medium contains the equation sum(X) = 1.0; set reducedX=true if only one substance (see docu for details)";
    fixedX::Bool                                 # "= true if medium contains the equation X = reference_X";
    reference_p::Float64                         # "Reference pressure of Medium: default 1 atmosphere";
    reference_T::Float64                         # "Reference temperature of Medium: default 25 deg Celsius";
    reference_X::AbstractVector                  # "Reference mass fractions of medium";
    p_default::Float64                           # "Default value for pressure of medium (for initialization)";
    T_default::Float64                           # "Default value for temperature of medium (for initialization)";
    h_default::Float64                           # "Default value for specific enthalpy of medium (for initialization)";
    X_default::Vector{Float64}                   # "Default value for specific enthalpy of medium (for initialization)";
    nS::Int                                      # "Number of substances"
    nX::Int                                      # "Number of mass fractions"
    nXi::Int                                     # "Default value for mass fractions of medium (for initialization)"
    nC::Int                                      # "Number of extra (outside of standard mass-balance) transported properties"
    C_nominal::Vector{Float64}                   # "Default for the nominal values for the extra properties"

    function FluidInfos(; mediumName=Missing,
                          substanceNames=[mediumName],
                          extraPropertiesNames=fill("",0),
                          ThermoStates=Missing,
                          baseProperties=Missing,
                          singleState=Missing,
                          reducedX=true,
                          fixedX=false,
                          reference_p=101325,
                          reference_T=298.15,
                          reference_X=fill(1/length(substanceNames),length(substanceNames)),
                          p_default=101325,
                          T_default=293.15,
                          h_default=NaN,
                          X_default=reference_X,
                          C_nominal=1e-6*ones(length(extraPropertiesNames)))
         nS  = length(substanceNames)
         nXi = fixedX ? 0 : ( reducedX ? nS-1 : nS )
         nC  = length(extraPropertiesNames)

         new(mediumName, substanceNames, extraPropertiesNames,
             ThermoStates, baseProperties, singleState, reducedX, fixedX, reference_p,
             reference_T, reference_X, p_default, T_default, h_default, X_default,
             nS, nS, nXi, nC, C_nominal)
    end
end

"""
    fluidLimits = FluidLimits(; TMIN=NaN, TMAX=NaN, DMIN=NaN, DMAX=NaN, PMIN=NaN, PMAX=NaN,
                                HMIN=NaN, HMAX=NaN, SMIN=NaN, SMAX=NaN)

Generate a new `FluidLimits` object, containing the validity limits of the medium.
"""
mutable struct FluidLimits
    TMIN::Float64  # "Minimum temperature";
    TMAX::Float64  # "Maximum temperature";
    DMIN::Float64  # "Minimum density";
    DMAX::Float64  # "Maximum density";
    PMIN::Float64  # "Minimum pressure";
    PMAX::Float64  # "Maximum pressure";
    HMIN::Float64  # "Minimum enthalpy";
    HMAX::Float64  # "Maximum enthalpy";
    SMIN::Float64  # "Minimum entropy";
    SMAX::Float64  # "Maximum entropy";

    FluidLimits(; TMIN=NaN, TMAX=NaN, DMIN=NaN, DMAX=NaN, PMIN=NaN, PMAX=NaN,
                  HMIN=NaN, HMAX=NaN, SMIN=NaN, SMAX=NaN) =
              new(TMIN, TMAX, DMIN, DMAX, PMIN, PMAX, HMIN, HMAX, SMIN, SMAX)
end

"""
    fluidConstants = BasicFluidConstants(;iupacName="", casRegistryNumber="",
                         chemicalFormula="", structureFormula="", molarMass=NaN)

Generate a `BasicFluidConstants <: AbstractFluidConstants` object
containing the minimal information about the standard data of the medium.
"""
mutable struct BasicFluidConstants <: AbstractFluidConstants
    iupacName::AbstractString           # "Complete IUPAC name (or common name, if non-existent)";
    casRegistryNumber::AbstractString   # "Chemical abstracts sequencing number (if it exists)";
    chemicalFormula::AbstractString     # "Chemical formula, (brutto, nomenclature according to Hill";
    structureFormula::AbstractString    # "Chemical structure formula";
    molarMass::Float64                  # "Molar mass";

    BasicFluidConstants(;iupacName="", casRegistryNumber="", chemicalFormula="", structureFormula="", molarMass=NaN) =
       new(iupacName, casRegistryNumber, chemicalFormula, structureFormula, molarMass)
end

"""
    fluidConstants = IdealGasFluidConstants(; iupacName="", casRegistryNumber="",
          chemicalFormula="", structureFormula="", molarMass=NaN,
          criticalTemperature=NaN, criticalPressure=NaN, criticalMolarVolume=NaN,
          acentricFactor=NaN, meltingPoint=NaN, normalBoilingPoint=NaN, dipoleMoment=NaN,
          hasIdealGasHeatCapacity=false, hasCriticalData=false, hasDipoleMoment=false,
          hasFundamentalEquation=false, hasLiquidHeatCapacity=false, hasSolidHeatCapacity=false,
          hasAccurateViscosityData=false, hasAccurateConductivityData=false,
          hasVapourPressureCurve=false, hasAcentricFactor=false,
          HCRIT0=0.0, SCRIT0=0.0, deltah=0.0, deltas=0.0)

Generate a `IdealGasFluidConstants <: AbstractFluidConstants` object
containing the minimal information about the standard data of ideal gas media
(critical, triple, molecular and other standard data).
"""
mutable struct IdealGasFluidConstants <: AbstractFluidConstants
    iupacName::AbstractString           # "Complete IUPAC name (or common name, if non-existent)";
    casRegistryNumber::AbstractString   # "Chemical abstracts sequencing number (if it exists)";
    chemicalFormula::AbstractString     # "Chemical formula, (brutto, nomenclature according to Hill";
    structureFormula::AbstractString    # "Chemical structure formula";
    molarMass::Float64                  # "Molar mass";

    criticalTemperature::Float64        # "Critical temperature";
    criticalPressure::Float64           # "Critical pressure";
    criticalMolarVolume::Float64        # "Critical molar Volume";
    acentricFactor::Float64             # "Pitzer acentric factor";
    meltingPoint::Float64               # "Melting point at 101325 Pa";
    normalBoilingPoint::Float64         # "Normal boiling point (at 101325 Pa)";
    dipoleMoment::Float64               # "Dipole moment of molecule in Debye (1 debye = 3.33564e10-30 C.m)";

    hasIdealGasHeatCapacity::Bool       # "True if ideal gas heat capacity is available";
    hasCriticalData::Bool               # "True if critical data are known";
    hasDipoleMoment::Bool               # "True if a dipole moment known";
    hasFundamentalEquation::Bool        # "True if a fundamental equation";
    hasLiquidHeatCapacity::Bool         # "True if liquid heat capacity is available";
    hasSolidHeatCapacity::Bool          # "True if solid heat capacity is available";
    hasAccurateViscosityData::Bool      # "True if accurate data for a viscosity function is available";
    hasAccurateConductivityData::Bool   # "True if accurate data for thermal conductivity is available";
    hasVapourPressureCurve::Bool        # "True if vapour pressure data, e.g., Antoine coefficients are known";
    hasAcentricFactor::Bool             # "True if Pitzer accentric factor is known";

    HCRIT0::Float64                     # "Critical specific enthalpy of the fundamental equation";
    SCRIT0::Float64                     # "Critical specific entropy of the fundamental equation";
    deltah::Float64                     # "Difference between specific enthalpy model (h_m) and f.eq. (h_f) (h_m - h_f)";
    deltas::Float64                     # "Difference between specific enthalpy model (s_m) and f.eq. (s_f) (s_m - s_f)";

    IdealGasFluidConstants(; iupacName="", casRegistryNumber="", chemicalFormula="", structureFormula="", molarMass=NaN,
                             criticalTemperature=NaN, criticalPressure=NaN, criticalMolarVolume=NaN,
                             acentricFactor=NaN, meltingPoint=NaN, normalBoilingPoint=NaN, dipoleMoment=NaN,
                             hasIdealGasHeatCapacity=false, hasCriticalData=false, hasDipoleMoment=false,
                             hasFundamentalEquation=false, hasLiquidHeatCapacity=false, hasSolidHeatCapacity=false,
                             hasAccurateViscosityData=false, hasAccurateConductivityData=false,
                             hasVapourPressureCurve=false, hasAcentricFactor=false,
                             HCRIT0=0.0, SCRIT0=0.0, deltah=0.0, deltas=0.0) =
        new(iupacName, casRegistryNumber, chemicalFormula, structureFormula, molarMass,
            criticalTemperature, criticalPressure, criticalMolarVolume, acentricFactor, meltingPoint,
            normalBoilingPoint, dipoleMoment, hasIdealGasHeatCapacity, hasCriticalData, hasDipoleMoment,
            hasFundamentalEquation, hasLiquidHeatCapacity, hasSolidHeatCapacity, hasAccurateViscosityData,
            hasAccurateConductivityData, hasVapourPressureCurve, hasAcentricFactor,
            HCRIT0, SCRIT0, deltah, deltas)
end

"""
    state = setState_pTX(medium, p,T,X)

Generate a state object for medium `medium::AbstractMedium` for
pressure `p` [Pa], temperature `T` [K] and mass fractions vector `X` or `Xi`.
"""
setState_pTX(m::AbstractMedium,p,T,X) = undefinedFunction("setState_pTX", m)

"""
    state = setState_phX(medium, p,h,X)

Generate a state object for medium `medium::AbstractMedium` for
pressure `p` [Pa], specific enthalpy `h` [J/kg]] and mass fractions vector `X` or `Xi`.
"""
setState_phX(m::AbstractMedium,p,h,X) = undefinedFunction("setState_phX", m)

"""
    state = setState_psX(medium, p,s,X)

Generate a state object for medium `medium::AbstractMedium` for
pressure `p` [Pa], specific entropy `s` [J/(kg*K)] and mass fractions vector `X`  or `Xi`.
"""
setState_psX(m::AbstractMedium,p,s,X) = undefinedFunction("setState_psX", m)

"""
    state = setState_dTX(medium, d,T,X)

Generate a state object for medium `medium::AbstractMedium` for
density `d` [kg/m^3], temperature `T` [K] and mass fractions vector `X` or `Xi`.
"""
setState_dTX(m::AbstractMedium,d,T,X) = undefinedFunction("setState_dTX", m)

### Medium functions ---------------------------------------------------------------------------------

"p = pressure(medium,state) - return pressure for `medium` from `state` in [Pa]"
pressure(m::AbstractMedium, state::ThermodynamicState) = undefinedFunction("pressure", m)

"T = temperature(medium,state) - return temperature for `medium` from `state` in [K]"
temperature(m::AbstractMedium, state::ThermodynamicState) = undefinedFunction("temperature", m)

"d = density(medium,state) - return density for `medium` from `state` in [kg/m^3]"
density(m::AbstractMedium, state::ThermodynamicState) = undefinedFunction("density", m)

"h = specificEnthalpy(medium,state) - return specific enthalpy for `medium` from `state` in [J/kg]"
specificEnthalpy(m::AbstractMedium, state::ThermodynamicState) = undefinedFunction("specificEnthalpy", m)

"u = specificInternalEnergy(medium,state) - return specific internal energy from `medium` at `state` in [J/kg]"
specificInternalEnergy(m::AbstractMedium, state::ThermodynamicState) = undefinedFunction("specificInternalEnergy", m)

"cp = specificHeatCapacityCp(medium,state) - return specific heat capacity at constant pressure for `medium` from `state` in [J/(kg*K)]"
specificHeatCapacityCp(m::AbstractMedium, state::ThermodynamicState) = undefinedFunction("specificHeatCapacity", m)

### Medium functions expressed from the functions above -------------------------------------------------

"h = specificEnthalpy_pTX(medium,p,T,X) - return specific enthalpy for `medium` from p, T, and X or Xi in [J/kg]"
specificEnthalpy_pTX(m::AbstractMedium, p,T,X) = specficEnthalpy(m, setState_pTX(m,p,T,X))

"T = temperature_phX(medium,p,h,X) - return temperature for `medium` from p, h, and X or Xi in [K]"
temperature_phX(m::AbstractMedium, p,h,X) = temperature(m, setState_phX(m,p,h,X))

"d = density_pTX(medium,p,T,X) - return density for `medium` from p, T, and X or Xi in [kg/m^3]"
density_pTX(m::AbstractMedium, p,T,X) = density(m, setState_pTX(m,p,T,X))

"d = density_phX(medium,p,h,X) - return density for `medium` from p, h, and X or Xi in [kg/m^3]"
density_phX(m::AbstractMedium, p,h,X) = density(m, setState_phX(m,p,h,X))

### Other functions operating on the medium ---------------------------------------------------------------------------------

to_DensityDisplayUnit(d) = d*1e-3
from_degC(Celsius)       = Celsius + 273.15

"""
    dict = standardCharacteristics(medium::AbstractMedium)

Return a `dict::dict{AbstractString,Any}` dictionary with the most
important characteristics of the medium as vectors.
"""
standardCharacteristics(m::AbstractMedium)::dict{AbstractString,Any} = undefinedFunction("standardCharacteristics", m)

"""
    standardPlot(medium::AbstractMedium; figure=1)

Plot the `standardCharacteristics(medium)` of the medium.
"""
standardPlot(m::AbstractMedium; figure=1) = undefinedFunction("standardPlot", m)

const T_zero = -273.15
to_degC(T_in_K) = T_in_K + Modelica.Constants.T_zero;


#####
##### `AbstractSynthController`
#####

abstract type AbstractSynthController end

#####
##### `PitchController`
#####

struct PitchController{
    S<:AbstractFloat,
    T<:NTuple{<:Any,Pair{<:AbstractOscillator,<:Any}},
} <: AbstractSynthController
    base_frequency::Ref{S}
    to_set::T

    function PitchController(
        base_frequency::Real,
        to_set::Pair{<:AbstractOscillator,<:Any}...,
    )
        base_frequency = float(base_frequency)
        _setfrequency!(to_set, base_frequency)
        S = typeof(base_frequency)
        T = typeof(to_set)
        return new{S,T}(Ref(base_frequency), to_set)
    end
end

function PitchController(
    base_frequency::Real,
    to_set::Pair{<:AbstractOscillator,<:Real}...,
)
    to_set = map(((osc, c),) -> (osc => Base.Fix1(*, c)), to_set)
    return PitchController(base_frequency, to_set...)
end

function _setfrequency!(
    to_set::NTuple{<:Any,Pair{<:AbstractOscillator,<:Any}},
    frequency::Real
)
    for (osc, f) in to_set
        setfrequency!(osc, f(frequency))
    end
end

getfrequency(p::PitchController) = p.base_frequency[]
function setfrequency!(p::PitchController, frequency::Real)
    _setfrequency!(p.to_set, frequency)
    p.base_frequency[] = float(frequency)
end

@enum KeyState::Bool PRESSED=true RELEASED=false

struct Envelope{F,S<:Real,T<:AbstractFloat}
end