text:
struct MaybeKnown
  hint::Int
  sym::Symbol
  known::Bool
end
struct Loop
  itersymbol::Symbol
  start::MaybeKnown
  stop::MaybeKnown
  step::MaybeKnown
  rangesym::Symbol# === Symbol("") means loop is static
  lensym::Symbol
end

struct UnrollSymbols
  u₁loopsym::Symbol
  u₂loopsym::Symbol
  vloopsym::Symbol
end
struct UnrollArgs
  u₁loop::Loop
  u₂loop::Loop
  vloop::Loop
  u₁::Int
  u₂max::Int
  suffix::Int # -1 means not tiled
end
UnPack.unpack(ua::UnrollArgs, ::Val{:u₁loopsym}) = getfield(getfield(ua, :u₁loop), :itersymbol)
UnPack.unpack(ua::UnrollArgs, ::Val{:u₂loopsym}) = getfield(getfield(ua, :u₂loop), :itersymbol)
UnPack.unpack(ua::UnrollArgs, ::Val{:vloopsym}) = getfield(getfield(ua, :vloop), :itersymbol)
UnPack.unpack(ua::UnrollArgs, ::Val{:u₁step}) = getfield(getfield(ua, :u₁loop), :step)
UnPack.unpack(ua::UnrollArgs, ::Val{:u₂step}) = getfield(getfield(ua, :u₂loop), :step)
UnPack.unpack(ua::UnrollArgs, ::Val{:vstep}) = getfield(getfield(ua, :vloop), :step)

struct UnrollSpecification
  u₁loopnum::Int
  u₂loopnum::Int
  vloopnum::Int
  u₁::Int
  u₂::Int
end
# UnrollSpecification(ls::LoopSet, u₁loop::Loop, vloopsym::Symbol, u₁, u₂) = UnrollSpecification(ls, u₁loop.itersymbol, vloopsym, u₁, u₂)
function UnrollSpecification(us::UnrollSpecification, u₁, u₂)
  @unpack u₁loopnum, u₂loopnum, vloopnum = us
  UnrollSpecification(u₁loopnum, u₂loopnum, vloopnum, u₁, u₂)
end
# function UnrollSpecification(us::UnrollSpecification; u₁ = us.u₁, u₂ = us.u₂)
#     @unpack u₁loopnum, u₂loopnum, vloopnum = us
#     UnrollSpecification(u₁loopnum, u₂loopnum, vloopnum, u₁, u₂)
# end
isunrolled1(us::UnrollSpecification, n::Int) = us.u₁loopnum == n
isunrolled2(us::UnrollSpecification, n::Int) = !isunrolled1(us, n) && us.u₂loopnum == n
isvectorized(us::UnrollSpecification, n::Int) = us.vloopnum == n
function unrollfactor(us::UnrollSpecification, n::Int)
  @unpack u₁loopnum, u₂loopnum, u₁, u₂ = us
  (u₁loopnum == n) ? u₁ : ((u₂loopnum == n) ? u₂ : 1)
end
function pushexpr!(ex::Expr, mk::MaybeKnown)
  if isknown(mk)
    push!(ex.args, staticexpr(gethint(mk)))
  else
    push!(ex.args, getsym(mk))
  end
  nothing
end
pushexpr!(ex::Expr, x::Union{Symbol,Expr}) = (push!(ex.args, x); nothing)
pushexpr!(ex::Expr, x::Integer) = (push!(ex.args, staticexpr(convert(Int, x))); nothing)
MaybeKnown(x::Integer) = MaybeKnown(convert(Int, x), Symbol("##UNDEFINED##"), true)
MaybeKnown(x::Integer, default::Int) = MaybeKnown(x)
MaybeKnown(x::Symbol, default::Int) = MaybeKnown(default, x, false)

isknown(mk::MaybeKnown) = getfield(mk, :known)
getsym(mk::MaybeKnown) = getfield(mk, :sym)
gethint(mk::MaybeKnown) = getfield(mk, :hint)
Base.isone(mk::MaybeKnown) = isknown(mk) && isone(gethint(mk))
Base.iszero(mk::MaybeKnown) = isknown(mk) && iszero(gethint(mk))
gethint(a::Integer) = a

function Loop(
  itersymbol::Symbol, start::Union{Int,Symbol}, stop::Union{Int,Symbol}, step::Union{Int,Symbol},
  rangename::Symbol, lensym::Symbol
  )
  Loop(itersymbol, MaybeKnown(start, 1), MaybeKnown(stop, 1024), MaybeKnown(step, 1), rangename, lensym)
end
startstopΔ(loop::Loop) = gethint(last(loop)) - gethint(first(loop))
function Base.length(loop::Loop)
  l = startstopΔ(loop)
  s = gethint(step(loop))
  (isone(s) ? l : cld(l, s)) + 1
end
Base.first(l::Loop) = getfield(l, :start)
Base.last(l::Loop) = getfield(l, :stop)
Base.step(l::Loop) = getfield(l, :step)

isstaticloop(l::Loop) = isknown(first(l)) & isknown(last(l)) & isknown(step(l))
unitstep(l::Loop) = isone(step(l))

function startloop(loop::Loop, itersymbol, staticinit::Bool=false)
  start = first(loop)
  if isknown(start)
    if staticinit
      Expr(:(=), itersymbol, staticexpr(gethint(start)))
    else
      Expr(:(=), itersymbol, gethint(start))
    end
  else
    Expr(:(=), itersymbol, Expr(:call, lv(:Int), getsym(start)))
  end
end

pushmulexpr!(q, a, b) = (push!(q.args, mulexpr(a, b)); nothing)
function pushmulexpr!(q, a, b::Integer)
  isone(b) ? push!(q.args, a) : push!(q.args, mulexpr(a, b))
  nothing
end

# function arithmetic_expr(f, a, b)
#     call = Expr(:call, lv(f))
#     if isa(a, MaybeKnown)
#         pushexpr!(
# end
isknown(x::Union{Symbol,Expr}) = false
isknown(x::Integer) = true
addexpr(a,b) = arithmeticexpr(+, :vadd_nsw, a, b)
subexpr(a,b) = arithmeticexpr(-, :vsub_nsw, a, b)
mulexpr(a,b) = arithmeticexpr(*, :vmul_nsw, a, b)
lazymulexpr(a,b) = arithmeticexpr(*, :lazymul, a, b)
function arithmeticexpr(op, f, a::Union{Integer,MaybeKnown}, b::Union{Integer,MaybeKnown})
  if isknown(a) & isknown(b)
    return staticexpr(op(gethint(a), gethint(b)))
  else
    return _arithmeticexpr(f, a, b)
  end
end
arithmeticexpr(op, f, a, b) = _arithmeticexpr(f, a, b)
function _arithmeticexpr(f, a, b)
  ex = Expr(:call, lv(f))
  pushexpr!(ex, a)
  pushexpr!(ex, b)
  return ex
end
mulexpr(a,b,c) = arithmeticexpr(*, 1, :vmul_nsw, a, b, c)
addexpr(a,b,c) = arithmeticexpr(+, 0, :vadd_nsw, a, b, c)
function arithmeticexpr(op, init, f, a, b, c)
  ex = Expr(:call, lv(f))
  p = init
  if isknown(a)
    p = op(p, gethint(a))
    known = 1
  else
    pushexpr!(ex, a)
    known = 0
  end
  if isknown(b)
    p = op(p, gethint(b))
    known += 1
  else
    pushexpr!(ex, b)
  end
  if isknown(c)
    p = op(p, gethint(c))
    known += 1
  else
    if known == 0
      ex = Expr(:call, lv(f), ex)
    end
    pushexpr!(ex, c)
  end
  if known == 3
    return staticexpr(p)
  else
    if known == 2
      pushexpr!(ex, p)
      return ex
    elseif known == 1
      if ((op === (+)) && (p == 0)) || ((op === (*)) && (p == 1))
        return ex
      else
        return Expr(:call, lv(f), ex, staticexpr(p))
      end
    else#known == 0
      return ex
    end
  end
end

function addexpr(ex, incr::Integer)
  if incr > 0
    f = :vadd_nsw
  else
    f = :vsub_nsw
    incr = -incr
  end
  expr = Expr(:call, lv(f))
  pushexpr!(expr, ex)
  pushexpr!(expr, convert(Int, incr))
  expr
end
staticmulincr(ptr, incr) = Expr(:call, lv(:staticmul), Expr(:call, :eltype, ptr), incr)

@inline cmpend(i::Int, r::CloseOpen) = i < getfield(r,:upper)
@inline cmpend(i::Int, r::AbstractUnitRange) = i ≤ last(r)
@inline cmpend(i::Int, r::AbstractRange) = i ≤ last(r)

@inline vcmpend(i::Int, r::CloseOpen, ::StaticInt{W}) where {W} = i ≤ vsub_nsw((getfield(r,:upper) % Int), W)
@inline vcmpendzs(i::Int, r::CloseOpen, ::StaticInt{W}) where {W} = i ≠ ((getfield(r,:upper) % Int) &  (-W))
@inline vcmpend(i::Int, r::AbstractUnitRange, ::StaticInt{W}) where {W} = i ≤ vsub_nsw(last(r), W-1)
@inline vcmpendzs(i::Int, r::AbstractUnitRange, ::StaticInt{W}) where {W} = i ≠ (length(r) &  (-W))
# i = 0
# i += 4*3 # i = 12
@inline vcmpend(i::Int, r::AbstractRange, ::StaticInt{W}) where {W} =   i ≤ vsub_nsw(last(r), vsub_nsw(W*step(r), 1))
@inline vcmpendzs(i::Int, r::AbstractRange, ::StaticInt{W}) where {W} = i ≤ vsub_nsw(last(r), vsub_nsw(W*step(r), 1))

function staticloopexpr(loop::Loop)
  f = first(loop)
  s = step(loop)
  l = last(loop)
  if isone(s)
    Expr(:call, GlobalRef(Base, :(:)), staticexpr(gethint(f)), staticexpr(gethint(l)))
  else
    Expr(:call, GlobalRef(Base, :(:)), staticexpr(gethint(f)), staticexpr(gethint(s)), staticexpr(gethint(l)))
  end
end
function vec_looprange(loop::Loop, UF::Int, mangledname)
  fast = ispow2(UF) && iszero(first(loop))
  if loop.rangesym === Symbol("") # means loop is static
    vec_looprange(UF, mangledname, staticloopexpr(loop), fast)
  else
    vec_looprange(UF, mangledname, loop.rangesym, fast)
  end
end
function vec_looprange(UF::Int, mangledname, r::Union{Expr,Symbol}, zerostart::Bool)
  cmp = zerostart ? lv(:vcmpendzs) : lv(:vcmpend)
  if isone(UF)
    Expr(:call, cmp, mangledname, r, VECTORWIDTHSYMBOL)
  else
    Expr(:call, cmp, mangledname, r, mulexpr(VECTORWIDTHSYMBOL, UF))
  end
end
function looprange(loop::Loop, UF::Int, mangledname)
  if loop.rangesym === Symbol("") # means loop is static
    looprange(UF, mangledname, staticloopexpr(loop))
  else
    looprange(UF, mangledname, loop.rangesym)
  end
end
function looprange(UF::Int, mangledname, r::Union{Expr,Symbol})
  if isone(UF)
    Expr(:call, lv(:cmpend), mangledname, r)
  else
    Expr(:call, lv(:vcmpend), mangledname, r, staticexpr(UF))
  end
end

function terminatecondition(
    loop::Loop, us::UnrollSpecification, n::Int, mangledname::Symbol, inclmask::Bool, UF::Int = unrollfactor(us, n)
)
  if !isvectorized(us, n)
    looprange(loop, UF, mangledname)
  elseif inclmask
    looprange(loop, 1, mangledname)
  else
    vec_looprange(loop, UF, mangledname) # may not be u₂loop
  end
end

function incrementloopcounter(us::UnrollSpecification, n::Int, mangledname::Symbol, UF::Int, l::Loop)
  incr = step(l)
  if isknown(incr)
    incrementloopcounter(us, n, mangledname, UF * gethint(incr))
  else
    incrementloopcounter(us, n, mangledname, UF, getsym(incr))
  end
end
function incrementloopcounter(us::UnrollSpecification, n::Int, mangledname::Symbol, UF::Int)
  if isvectorized(us, n)
    if isone(UF)
      Expr(:(=), mangledname, addexpr(VECTORWIDTHSYMBOL, mangledname))
    else
      Expr(:(=), mangledname, addexpr(mulexpr(VECTORWIDTHSYMBOL, staticexpr(UF)), mangledname))
    end
  else
    Expr(:(=), mangledname, addexpr(mangledname, UF))
  end
end
function incrementloopcounter(us::UnrollSpecification, n::Int, mangledname::Symbol, UF::Int, incr::Symbol)
  if isvectorized(us, n)
    if isone(UF)
      Expr(:(=), mangledname, addexpr(mulexpr(VECTORWIDTHSYMBOL, incr), mangledname))
    else
      Expr(:(=), mangledname, addexpr(mulexpr(mulexpr(VECTORWIDTHSYMBOL, staticexpr(UF)), incr), mangledname))
    end
  else
    Expr(:(=), mangledname, addexpr(mangledname, mulexpr(incr, UF)))
  end
end

function incrementloopcounter!(q, us::UnrollSpecification, n::Int, UF::Int, l::Loop)
  incr = step(l)
  if isknown(incr)
    incrementloopcounter!(q, us, n, UF * gethint(incr))
  else
    incrementloopcounter!(q, us, n, UF, getsym(incr))
  end
end
function incrementloopcounter!(q, us::UnrollSpecification, n::Int, UF::Int)
  if isvectorized(us, n)
    if isone(UF)
      push!(q.args, VECTORWIDTHSYMBOL)
    else
      push!(q.args, mulexpr(VECTORWIDTHSYMBOL, staticexpr(UF)))
    end
  else
    push!(q.args, staticexpr(UF))
  end
end
function incrementloopcounter!(q, us::UnrollSpecification, n::Int, UF::Int, incr::Symbol)
  if isvectorized(us, n)
    if isone(UF)
      push!(q.args, mulexpr(VECTORWIDTHSYMBOL, incr))
    else
      push!(q.args, mulexpr(mulexpr(VECTORWIDTHSYMBOL, staticexpr(UF)), incr))
    end
  else
    push!(q.args, mulexpr(staticexpr(UF), incr))
  end
end

# load/compute/store × isunrolled × istiled × pre/post loop × Loop number
struct LoopOrder <: AbstractArray{Vector{Operation},5}
  oporder::Vector{Vector{Operation}}
  loopnames::Vector{Symbol}
  bestorder::Vector{Symbol}
end
# function LoopOrder(N::Int)
#     LoopOrder(
#         [ Operation[] for _ ∈ 1:8N ],
#         Vector{Symbol}(undef, N), Vector{Symbol}(undef, N)
#     )
# end
LoopOrder() = LoopOrder(Vector{Operation}[],Symbol[],Symbol[])
Base.empty!(lo::LoopOrder) = foreach(empty!, lo.oporder)
function Base.resize!(lo::LoopOrder, N::Int)
  Nold = length(lo.loopnames)
  resize!(lo.oporder, 8N)
  for n ∈ 8Nold+1:8N
    lo.oporder[n] = Operation[]
  end
  resize!(lo.loopnames, N)
  resize!(lo.bestorder, N)
  lo
end
Base.size(lo::LoopOrder) = (2,2,2,length(lo.loopnames))
Base.@propagate_inbounds Base.getindex(lo::LoopOrder, i::Int) = lo.oporder[i]
Base.@propagate_inbounds Base.getindex(lo::LoopOrder, i::Vararg{Int,K}) where {K} = lo.oporder[LinearIndices(size(lo))[i...]]

@enum NumberType::Int8 HardInt HardFloat IntOrFloat INVALID

struct LoopStartStopManager
  terminators::Vector{Int}
  incrementedptrs::Vector{Vector{ArrayReferenceMeta}}
  uniquearrayrefs::Vector{ArrayReferenceMeta}
end
# Must make it easy to iterate
# outer_reductions is a vector of indices (within operation vectors) of the reduction operation, eg the vmuladd op in a dot product
# O(N) search is faster at small sizes
mutable struct LoopSet
  loopsymbols::Vector{Symbol}
  loopsymbol_offsets::Vector{Int}  # symbol loopsymbols[i] corresponds to loops[lso[i]+1:lso[i+1]] (CartesianIndex handling)
  loops::Vector{Loop}
  opdict::Dict{Symbol,Operation}
  operations::Vector{Operation} # Split them to make it easier to iterate over just a subset
  operation_offsets::Vector{Int}
  outer_reductions::Vector{Int} # IDs of reduction operations that need to be reduced at end.
  loop_order::LoopOrder
  preamble::Expr
  prepreamble::Expr # performs extractions that must be performed first, and don't need further registering
  preamble_symsym::Vector{Tuple{Int,Symbol}}
  preamble_symint::Vector{Tuple{Int,Tuple{Int,Int32,Bool}}} # (id,(intval,intsz,signed))
  preamble_symfloat::Vector{Tuple{Int,Float64}}
  preamble_zeros::Vector{Tuple{Int,NumberType}}
  preamble_funcofeltypes::Vector{Tuple{Int,Float64}}
  includedarrays::Vector{Symbol}
  includedactualarrays::Vector{Symbol}
  syms_aliasing_refs::Vector{Symbol}
  refs_aliasing_syms::Vector{ArrayReferenceMeta}
  cost_vec::Matrix{Float64}
  reg_pres::Matrix{Float64}
  included_vars::Vector{Bool}
  place_after_loop::Vector{Bool}
  unrollspecification::UnrollSpecification
  loadelimination::Bool
  lssm::LoopStartStopManager
  vector_width::Int
  symcounter::Int
  isbroadcast::Bool
  register_size::Int
  register_count::Int
  cache_linesize::Int
  cache_size::Tuple{Int,Int,Int}
  ureduct::Int
  equalarraydims::Vector{Tuple{Vector{Symbol},Vector{Int}}}
  omop::OffsetLoadCollection
  loopordermap::Vector{Int}
  loopindexesbit::Vector{Bool}
  validreorder::Vector{UInt8}
  mod::Symbol
  LoopSet() = new()
end

function UnrollArgs(ls::LoopSet, u₁::Int, unrollsyms::UnrollSymbols, u₂max::Int, suffix::Int)
  @unpack u₁loopsym, u₂loopsym, vloopsym = unrollsyms
  u₁loop = getloop(ls, u₁loopsym)
  u₂loop = u₂loopsym === Symbol("##undefined##") ? u₁loop : getloop(ls, u₂loopsym)
  vloop = getloop(ls, vloopsym)
  UnrollArgs(u₁loop, u₂loop, vloop, u₁, u₂max, suffix)
end

function cost_vec_buf(ls::LoopSet)
  cv = @view(ls.cost_vec[:,2])
  @inbounds for i ∈ 1:4
    cv[i] = 0.0
  end
  cv
end
function reg_pres_buf(ls::LoopSet)
  ps = @view(ls.reg_pres[:,2])
  @inbounds for i ∈ 1:4
    ps[i] = 0
  end
  ps[4] = reg_count(ls)
  ps
end
function save_tilecost!(ls::LoopSet)
  @inbounds for i ∈ 1:4
    ls.cost_vec[i,1] = ls.cost_vec[i,2]
    ls.reg_pres[i,1] = ls.reg_pres[i,2]
  end
  # ls.reg_pres[5,1] = ls.reg_pres[5,2]
end
function set_hw!(ls::LoopSet, rs::Int, rc::Int, cls::Int, l1::Int, l2::Int, l3::Int)
  ls.register_size = rs
  ls.register_count = rc
  ls.cache_linesize = cls
  ls.cache_size = (l1,l2,l3)
  # ls.opmask_register[] = omr
  nothing
end
available_registers() = ifelse(has_opmask_registers(), register_count(), register_count() - One())
function set_hw!(ls::LoopSet)
  set_hw!(
    ls, Int(register_size()), Int(available_registers()), Int(cache_linesize()),
    Int(cache_size(StaticInt(1))), Int(cache_size(StaticInt(2))), Int(cache_size(StaticInt(3)))
  )
end
reg_size(ls::LoopSet) = ls.register_size
reg_count(ls::LoopSet) = ls.register_count
cache_lnsze(ls::LoopSet) = ls.cache_linesize
cache_sze(ls::LoopSet) = ls.cache_size

pushprepreamble!(ls::LoopSet, ex) = push!(ls.prepreamble.args, ex)
function pushpreamble!(ls::LoopSet, op::Operation, v::Symbol)
  if v !== mangledvar(op)
    push!(ls.preamble_symsym, (identifier(op),v))
  end
  nothing
end

function integer_description(@nospecialize(v::Integer))::Tuple{Int,Int32,Bool}
  if v isa Bool
    ((v % Int)::Int, one(Int32), false)
  else
    ((v % Int)::Int, ((8sizeof(v))%Int32)::Int32, (v isa Signed)::Bool)
  end
end

function pushpreamble!(ls::LoopSet, op::Operation, v::Number)
  typ = v isa Integer ? HardInt : HardFloat
  id = identifier(op)
  if iszero(v)
    push!(ls.preamble_zeros, (id, typ))
  elseif v isa Integer
    push!(ls.preamble_symint, (id, integer_description(v)))
  else
    push!(ls.preamble_symfloat, (id, convert(Float64,v)))
  end
end
pushpreamble!(ls::LoopSet, ex::Expr) = push!(ls.preamble.args, ex)
# function pushpreamble!(ls::LoopSet, op::Operation, RHS::Expr)
#     c = gensym(:licmconst)
#     if RHS.head === :call && first(RHS.args) === :zero
#         push!(ls.preamble_zeros, (identifier(op), IntOrFloat))
#     elseif RHS.head === :call && first(RHS.args) === :one
#         push!(ls.preamble_funcofeltypes, (identifier(op), MULTIPLICATIVE_IN_REDUCTIONS))
#     else
#         pushpreamble!(ls, Expr(:(=), c, RHS))
#         pushpreamble!(ls, op, c)
#     end
#     nothing
# end
function zerotype(ls::LoopSet, op::Operation)
  opid = identifier(op)
  for (id,typ) ∈ ls.preamble_zeros
    id == opid && return typ
  end
  INVALID
end

includesarray(ls::LoopSet, array::Symbol) = array ∈ ls.includedarrays

function LoopSet(mod::Symbol)
  ls = LoopSet()
  ls.loopsymbols = Symbol[]
  ls.loopsymbol_offsets =  [0]
  ls.loops = Loop[]
  ls.opdict = Dict{Symbol,Operation}()
  ls.operations = Operation[]
  ls.operation_offsets = Int[0]
  ls.outer_reductions = Int[]
  ls.loop_order = LoopOrder()
  ls.preamble = Expr(:block)
  ls.prepreamble = Expr(:block)
  ls.preamble_symsym = Tuple{Int,Symbol}[]
  ls.preamble_symint = Tuple{Int,Tuple{Int,Int32,Bool}}[]
  ls.preamble_symfloat = Tuple{Int,Float64}[]
  ls.preamble_zeros = Tuple{Int,NumberType}[]
  ls.preamble_funcofeltypes = Tuple{Int,Float64}[]
  ls.includedarrays = Symbol[]
  ls.includedactualarrays = Symbol[]
  ls.syms_aliasing_refs = Symbol[]
  ls.refs_aliasing_syms = ArrayReferenceMeta[]
  ls.cost_vec = Matrix{Float64}(undef, 4, 2)
  ls.reg_pres = Matrix{Float64}(undef, 4, 2)
  ls.included_vars = Bool[]
  ls.place_after_loop = Bool[]
  ls.unrollspecification
  ls.loadelimination = false
  ls.vector_width = 0
  ls.symcounter = 0
  ls.isbroadcast = 0
  ls.register_size = 0
  ls.register_count = 0
  ls.cache_linesize = 0
  ls.cache_size = (0,0,0)
  ls.ureduct = -1
  ls.equalarraydims = Tuple{Vector{Symbol},Vector{Int}}[]
  ls.omop = OffsetLoadCollection()
  ls.loopordermap =  Int[]
  ls.loopindexesbit = Bool[]
  ls.validreorder = UInt8[]
  ls.mod = mod
  ls
end

"""
  Used internally to create symbols unique for this loopset.
  This is used so that identical loops will create identical `_turbo_!` calls in the macroexpansions, hopefully reducing recompilation.
  """
gensym!(ls::LoopSet, s) = Symbol("###$(s)###$(ls.symcounter += 1)###")

function fill_children!(ls::LoopSet)
  for op ∈ operations(ls)
    empty!(children(op))
    for opp ∈ parents(op)
      push!(children(opp), op)
    end
  end
end
function cacheunrolled!(ls::LoopSet, u₁loop::Symbol, u₂loop::Symbol, vloopsym::Symbol)
  vloop = getloop(ls, vloopsym)
  for op ∈ operations(ls)
    setunrolled!(ls, op, u₁loop, u₂loop, vloopsym)
    if accesses_memory(op)
      rc = rejectcurly(ls, op, u₁loop, vloopsym)
      op.rejectcurly = rc
      if rc
        op.rejectinterleave = true
      else
        omop = ls.omop
        batchid, opind = omop.batchedcollectionmap[identifier(op)]
        op.rejectinterleave = ((batchid == 0) || (!isvectorized(op))) || rejectinterleave(ls, op, vloop, omop.batchedcollections[batchid])
      end
    end
  end
end
function setunrolled!(ls::LoopSet, op::Operation, u₁loopsym::Symbol, u₂loopsym::Symbol, vectorized::Symbol)
  u₁::Bool = u₂::Bool = v::Bool = false
  for ld ∈ loopdependencies(op)
    u₁ |= ld === u₁loopsym
    u₂ |= ld === u₂loopsym
    v  |= ld === vectorized
  end
  if isconstant(op)
    for opp ∈ children(op)
      u₁ = u₁ &&  u₁loopsym ∈ loopdependencies(opp)
      u₂ = u₂ &&  u₂loopsym ∈ loopdependencies(opp)
      v  = v  && vectorized ∈ loopdependencies(opp)
    end
    if isouterreduction(ls, op) ≠ -1 && !all((u₁,u₂,v))
      opv = true
      for opp ∈ parents(op)
        if iscompute(opp) && instruction(opp).instr ≢ :identity
          opv = false
          break
        end
      end
      if opv
        if !u₁ && u₁loopsym ∈ reduceddependencies(op)
          u₁ = true
        end
        if !u₂ && u₂loopsym ∈ reduceddependencies(op)
          u₂ = true
        end
        if !v && vectorized ∈ reduceddependencies(op)
          v = true
        end
      end
    end
  end
  op.u₁unrolled = u₁
  op.u₂unrolled = u₂
  op.vectorized = v
  nothing
end

rejectcurly(op::Operation) = op.rejectcurly
rejectinterleave(op::Operation) = op.rejectinterleave
num_loops(ls::LoopSet) = length(ls.loops)
function oporder(ls::LoopSet)
  N = length(ls.loop_order.loopnames)
  reshape(ls.loop_order.oporder, (2,2,2,N))
end
names(ls::LoopSet) = ls.loop_order.loopnames
reversenames(ls::LoopSet) = ls.loop_order.bestorder
function getloopid_or_nothing(ls::LoopSet, s::Symbol)
  for (loopnum,sym) ∈ enumerate(ls.loopsymbols)
    s === sym && return loopnum
  end
end

getloopid(ls::LoopSet, s::Symbol) = getloopid_or_nothing(ls, s)::Int
getloop(ls::LoopSet, i::Integer) = ls.loops[ls.loopordermap[i]] # takes nest level after reordering
getloop_from_id(ls::LoopSet, i::Integer) = ls.loops[i] # takes w/ respect to original loop order.
getloop(ls::LoopSet, s::Symbol) = getloop_from_id(ls, getloopid(ls, s))
getloopsym(ls::LoopSet, i::Integer) = ls.loopsymbols[i]
Base.length(ls::LoopSet, s::Symbol) = length(getloop(ls, s))
function init_loop_map!(ls::LoopSet)
  @unpack loopordermap = ls
  order = names(ls)
  resize!(loopordermap, length(order))
  for (i,o) ∈ enumerate(order)
    loopordermap[i] = getloopid(ls,o)
  end
  nothing
end

# isstaticloop(ls::LoopSet, s::Symbol) = isstaticloop(getloop(ls,s))
# looprangehint(ls::LoopSet, s::Symbol) = length(getloop(ls, s))
# looprangesym(ls::LoopSet, s::Symbol) = getloop(ls, s).rangesym

"""
  getop only works while construction a LoopSet object. You cannot use it while lowering.
  """
getop(ls::LoopSet, var::Number, elementbytes) = add_constant!(ls, var, elementbytes)
function getop(ls::LoopSet, var::Symbol, elementbytes::Int)
  get!(ls.opdict, var) do
    add_constant!(ls, var, elementbytes)
  end
end
function getop(ls::LoopSet, var::Symbol, deps, elementbytes::Int)
  get!(ls.opdict, var) do
    add_constant!(ls, var, deps, gensym!(ls, "constant"), elementbytes)
  end
end
getop(ls::LoopSet, i::Int) = ls.operations[i]

findop(ls::LoopSet, s::Symbol) = findop(operations(ls), s)
function findop(ops::Vector{Operation}, s::Symbol)
  for op ∈ ops
    name(op) === s && return op
  end
  throw(ArgumentError("Symbol $s not found."))
end

# """
# Returns an operation with the same name as `s`.
# """
# function getoperation(ls::LoopSet, s::Symbol)
#     for op ∈ Iterators.Reverse(operations(ls))
#         name(op) === s && return op
#     end
#     throw("Symbol $s not found among operations(ls).")
# end

function Operation(
  ls::LoopSet, variable, elementbytes, instruction,
  node_type, dependencies, reduced_deps, parents, ref = NOTAREFERENCE
  )
  Operation(
    length(operations(ls)), variable, elementbytes, instruction,
    node_type, dependencies, reduced_deps, parents, ref
  )
end
function Operation(ls::LoopSet, variable, elementbytes, instr, optype, mpref::ArrayReferenceMetaPosition)
  Operation(length(operations(ls)), variable, elementbytes, instr, optype, mpref)
end

operations(ls::LoopSet) = ls.operations

function getconstvalues(ls::LoopSet, opparents::Vector{Operation})::Tuple{Bool,Vector{Any}}
  vals = sizehint!(Any[], length(opparents))
  for i ∈ eachindex(opparents)
    pushconstvalue!(vals, ls, opparents[i]) && return true, vals
  end
  false, vals
end

function add_constant_compute!(ls::LoopSet, op::Operation, var::Symbol)::Operation
  op.node_type = constant
  instr = instruction(op)
  opparents = parents(op)
  if Base.sym_in(instr.instr, (:add_fast,:mul_fast,:sub_fast,:div_fast,:vfmadd_fast, :vfnmadd_fast, :vfmsub_fast, :vfnmsub_fast))
    getconstfailed, vals = getconstvalues(ls, opparents)
    if !getconstfailed
      f = instr.instr
      if f === :add_fast
        return add_constant!(ls, sum(vals), 8)::Operation
      elseif f === :mul_fast
        return add_constant!(ls, prod(vals), 8)::Operation
      elseif f === :sub_fast
        if length(opparents) == 2
          return add_constant!(ls, vals[1] - vals[2], 8)::Operation
        elseif length(opparents) == 1
          return add_constant!(ls, - vals[1], 8)::Operation
        end
      elseif f === :div_fast
        if length(opparents) == 2
          return add_constant!(ls, vals[1] / vals[2], 8)::Operation
        end
      elseif length(opparents) == 3
        T = typeof(sum(vals))
        if f === :vfmadd_fast
          return add_constant!(ls, T( (big(vals[1])*big(vals[2]) + big(vals[3]))), 8)::Operation
        elseif f === :vfnmadd_fast
          return add_constant!(ls, T(big(vals[3]) - big(vals[1])*big(vals[2])), 8)::Operation
        elseif f === :vfmsub_fast
          return add_constant!(ls, T((big(vals[1])*big(vals[2]) - big(vals[3]))), 8)::Operation
        elseif f === :vfnmsub_fast
          return add_constant!(ls, T(- (big(vals[1])*big(vals[2]) + big(vals[3]))), 8)::Operation
        end
      end
    end
  end
  opdef = callexpr(instr)
  mangledname = Symbol('#', instruction(op).instr, '#')
  while length(opparents) > 0
    oppname = name(popfirst!(opparents))
    mangledname = Symbol(mangledname, oppname, '#')
    push!(opdef.args, oppname)
  end
  op.mangledvariable = mangledname
  pushpreamble!(ls, Expr(:(=), name(op), opdef))
  op.instruction = LOOPCONSTANT
  push!(ls.preamble_symsym, (identifier(op), name(op)))
  _pushop!(ls, op, var)
end

function _pushop!(ls::LoopSet, op::Operation, var::Symbol)
  for opp ∈ operations(ls)
    if matches(op, opp)
      ls.opdict[var] = opp
      return opp
    end
  end
  push!(ls.operations, op)
  ls.opdict[var] = op
  op
end
function pushop!(ls::LoopSet, op::Operation, var::Symbol = name(op))
  if (iscompute(op) && length(loopdependencies(op)) == 0)
    add_constant_compute!(ls, op, var)
  else
    _pushop!(ls, op, var)
  end
end

function add_block!(ls::LoopSet, ex::Expr, elementbytes::Int, position::Int)
  for x ∈ ex.args
    x isa Expr || continue # be that general?
    x.head === :inbounds && continue
    push!(ls, x, elementbytes, position)
  end
end
function maybestatic!(expr::Expr)
  if expr.head === :call
    f = first(expr.args)
    if f === :length
      expr.args[1] = GlobalRef(ArrayInterface,:static_length)
    elseif f === :size && length(expr.args) == 3
      i = expr.args[3]
      if i isa Integer
        expr.args[1] = GlobalRef(ArrayInterface,:size)
        expr.args[3] = staticexpr(convert(Int,i)::Int)
      end
    else
      static_literals!(expr)
    end
  end
  expr
end
add_loop_bound!(ls::LoopSet, itersym::Symbol, bound::Union{Integer,Symbol}, upper::Bool, step::Bool)::MaybeKnown = MaybeKnown(bound, upper ? 1024 : 1)
function add_loop_bound!(ls::LoopSet, itersym::Symbol, bound::Expr, upper::Bool, step::Bool)::MaybeKnown
  maybestatic!(bound)
  N = gensym!(ls, string(itersym) * (upper ? "_loop_upper_bound" : (step ? "_loop_step" : "_loop_lower_bound")))
  pushprepreamble!(ls, Expr(:(=), N, bound))
  MaybeKnown(N, upper ? 1024 : 1)
end
static_literals!(s::Symbol) = s
function static_literals!(q::Expr)
  for (i,ex) ∈ enumerate(q.args)
    if ex isa Number
      q.args[i] = staticexpr(ex)
    elseif ex isa Expr
      static_literals!(ex)
    end
  end
  q
end
function range_loop!(ls::LoopSet, itersym::Symbol, l::MaybeKnown, u::MaybeKnown, s::MaybeKnown)
  rangename = gensym!(ls, "range"); lenname = gensym!(ls, "length")
  range = Expr(:call, :(:))
  pushexpr!(range, l)
  isone(s) || pushexpr!(range, s)
  pushexpr!(range, u)
  pushprepreamble!(ls, Expr(:(=), rangename, range))
  pushprepreamble!(ls, Expr(:(=), lenname, Expr(:call, GlobalRef(ArrayInterface,:static_length), rangename)))
  Loop(itersym, l, u, s, rangename, lenname)
end
function range_loop!(ls::LoopSet, r::Expr, itersym::Symbol)::Loop
  lower = r.args[2]
  sii::Bool = if length(r.args) == 3
    step = 1
    upper = r.args[3]
    true
  elseif length(r.args) == 4
    step = r.args[3]
    upper = r.args[4]
    isa(step, Integer)
  else
    throw("Literal ranges must have either 2 or 3 arguments.")
  end
  lii::Bool = lower isa Integer
  uii::Bool = upper isa Integer
  l::MaybeKnown = add_loop_bound!(ls, itersym, lower, false, false)
  u::MaybeKnown = add_loop_bound!(ls, itersym, upper, true, false)
  s::MaybeKnown = add_loop_bound!(ls, itersym, step, false, true)
  range_loop!(ls, itersym, l, u, s)
end
function oneto_loop!(ls::LoopSet, r::Expr, itersym::Symbol)::Loop
  otN = r.args[2]
  l = MaybeKnown(1, 0)
  s = MaybeKnown(1, 0)
  u::MaybeKnown = if otN isa Integer
    rangename = lensym = Symbol("")
    MaybeKnown(convert(Int, otN)::Int, 0)
  else
    otN isa Expr && maybestatic!(otN)
    lensym = N = gensym!(ls, "loop" * string(itersym))
    rangename = gensym!(ls, "range");
    pushprepreamble!(ls, Expr(:(=), N, otN))
    pushprepreamble!(ls, Expr(:(=), rangename, Expr(:call, :(:), staticexpr(1), N)))
    MaybeKnown(N, 1024)
  end
  Loop(itersym, l, u, s, rangename, lensym)
end

@inline _reverse(r) = maybestaticlast(r):-static_step(r):maybestaticfirst(r)
@inline canonicalize_range(r::OptionallyStaticUnitRange) = r
@inline function canonicalize_range(r::OptionallyStaticRange, ::StaticInt{S}) where {S}
  ifelse(ArrayInterface.gt(StaticInt{S}(), Zero()), r, _reverse(r))
end
@inline canonicalize_range(r::OptionallyStaticRange, s::Integer) = s > 0 ? r : _reverse(r)
@inline canonicalize_range(r::CloseOpen) = r
@inline canonicalize_range(r::AbstractUnitRange) = maybestaticfirst(r):maybestaticlast(r)
@inline canonicalize_range(r::OptionallyStaticRange) = canonicalize_range(r, static_step(r))
@inline canonicalize_range(r::AbstractRange) = canonicalize_range(maybestaticfirst(r):static_step(r):maybestaticlast(r))
@inline canonicalize_range(r::CartesianIndices) = CartesianIndices(map(canonicalize_range, r.indices))
@inline canonicalize_range(r::Base.OneTo{U}) where {U <: Unsigned} = One():last(r)

function misc_loop!(ls::LoopSet, r::Union{Expr,Symbol}, itersym::Symbol, staticstepone::Bool)::Loop
  rangename = gensym!(ls, "looprange" * string(itersym)); lenname = gensym!(ls, "looplen" * string(itersym));
  pushprepreamble!(ls, Expr(:(=), rangename, Expr(:call, lv(:canonicalize_range), :(@inbounds $(static_literals!(r))))))
  pushprepreamble!(ls, Expr(:(=), lenname, Expr(:call, GlobalRef(ArrayInterface,:static_length), rangename)))
  L = add_loop_bound!(ls, itersym, Expr(:call, lv(:maybestaticfirst), rangename), false, false)
  U = add_loop_bound!(ls, itersym, Expr(:call, lv(:maybestaticlast), rangename), true, false)
  if staticstepone
    Loop(itersym, L, U, MaybeKnown(1), rangename, lenname)
  else
    S = add_loop_bound!(ls, itersym, Expr(:call, lv(:static_step), rangename), false, true)
    Loop(itersym, L, U, S, rangename, lenname)
  end
end

function indices_loop!(ls::LoopSet, r::Expr, itersym::Symbol)::Loop
  if length(r.args) == 3
    arrays = r.args[2]
    dims = r.args[3]
    if isexpr(arrays, :tuple) && length(arrays.args) > 1 && all(s -> s isa Symbol, arrays.args)
      narrays =  length(arrays.args)::Int
      axessyms = Vector{Symbol}(undef, narrays)
      if dims isa Integer
        # ids = Vector{NTuple{2,Int}}(undef, narrays)
        vptrs = Vector{Symbol}(undef, narrays)
        mdims = fill(dims::Int, narrays)
        # _d::Int = dims
        for n ∈ 1:narrays
          a_s::Symbol = arrays.args[n]
          vptrs[n] = vptr(a_s)
          axessyms[n] = axsym = gensym!(ls, "#axes#$(a_s)#")
          pushprepreamble!(ls, Expr(:(=), axsym, Expr(:call, GlobalRef(ArrayInterface, :axes), a_s, staticexpr(dims::Int))))
          if n > 1
            axsym_prev = axessyms[n-1]
            pushprepreamble!(ls, Expr(:call, GlobalRef(VectorizationBase,:assume), Expr(:call, GlobalRef(Base,:(==)), Expr(:call, GlobalRef(ArrayInterface, :static_first), axsym), Expr(:call, GlobalRef(ArrayInterface, :static_first), axsym_prev))))
            pushprepreamble!(ls, Expr(:call, GlobalRef(VectorizationBase,:assume), Expr(:call, GlobalRef(Base,:(==)), Expr(:call, GlobalRef(ArrayInterface, :static_last), axsym), Expr(:call, GlobalRef(ArrayInterface, :static_last), axsym_prev))))
          end
        end
        push!(ls.equalarraydims, (vptrs, mdims))
      elseif isexpr(dims, :tuple) && length(dims.args) == narrays && all(i -> i isa Integer, dims.args)
        # ids = Vector{NTuple{2,Int}}(undef, narrays)
        vptrs = Vector{Symbol}(undef, narrays)
        mdims = Vector{Int}(undef, narrays)
        axessyms = Vector{Symbol}(undef, narrays)
        for n ∈ 1:narrays
          a_s::Symbol = arrays.args[n]
          vptrs[n] = vptr(a_s)
          mdim::Int = dims.args[n]
          mdims[n] = mdim
          axessyms[n] = axsym = gensym!(ls, "#axes#$(a_s)#")
          pushprepreamble!(ls, Expr(:(=), axsym, Expr(:call, GlobalRef(ArrayInterface, :axes), a_s, staticexpr(mdim))))
          if n > 1
            axsym_prev = axessyms[n-1]
            pushprepreamble!(ls, Expr(:call, GlobalRef(VectorizationBase,:assume), Expr(:call, GlobalRef(Base,:(==)), Expr(:call, GlobalRef(ArrayInterface, :static_first), axsym), Expr(:call, GlobalRef(ArrayInterface, :static_first), axsym_prev))))
            pushprepreamble!(ls, Expr(:call, GlobalRef(VectorizationBase,:assume), Expr(:call, GlobalRef(Base,:(==)), Expr(:call, GlobalRef(ArrayInterface, :static_last), axsym), Expr(:call, GlobalRef(ArrayInterface, :static_last), axsym_prev))))
          end
        end
        push!(ls.equalarraydims, (vptrs, mdims))
        # push!(ls.equalarraydims, ids)
      end
    end
  end
  misc_loop!(ls, r, itersym, true)
end

"""
  This function creates a loop, while switching from 1 to 0 based indices
  """
function register_single_loop!(ls::LoopSet, looprange::Expr)
  itersym = (looprange.args[1])::Symbol
  r = looprange.args[2]
  loop = if isexpr(r, :call)
    r = r::Expr        # julia#37342
    f = first(r.args)
    if f === :(:)
      range_loop!(ls, r, itersym)
    elseif f === :OneTo || isscopedname(f, :Base, :OneTo)
      oneto_loop!(ls, r, itersym)
    elseif f === :indices || (isexpr(f, :(.), 2) && (f.args[2] === QuoteNode(:indices)) && ((f.args[1] === :ArrayInterface) || (f.args[1] === :LoopVectorization)))
      indices_loop!(ls, r, itersym)
    else
      (f === :axes) && (r.args[1] = lv(:axes))
      misc_loop!(ls, r, itersym, (f === :eachindex) | (f === :axes))
    end
  elseif isa(r, Symbol)
    misc_loop!(ls, r, itersym, false)
  else
    throw(LoopError("Unrecognized loop range type: $r."))
  end
  add_loop!(ls, loop, itersym)
  nothing
end
function register_loop!(ls::LoopSet, looprange::Expr)
  if looprange.head === :block # multiple loops
    for lr ∈ looprange.args
      register_single_loop!(ls, lr::Expr)
    end
  else
    @assert looprange.head === :(=)
    register_single_loop!(ls, looprange)
  end
end
function add_loop!(ls::LoopSet, q::Expr, elementbytes::Int)
  register_loop!(ls, q.args[1]::Expr)
  body = q.args[2]::Expr
  position = length(ls.loopsymbols)
  if body.head === :block
    add_block!(ls, body, elementbytes, position)
  else
    push!(ls, q, elementbytes, position)
  end
end
function add_loop!(ls::LoopSet, loop::Loop, itersym::Symbol = loop.itersymbol)
  push!(ls.loopsymbols, itersym)
  push!(ls.loops, loop)
  nothing
end

# function instruction(x)
#     x isa Symbol ? x : last(x.args).value
# end
# instruction(ls::LoopSet, f::Symbol) = instruction!(ls, f)
function instruction!(ls::LoopSet, x::Expr)
  # x isa Symbol && return x
  if x.head === :$
    _x = only(x.args)
    _x isa Symbol && return instruction!(ls, _x)
    @assert _x isa Expr
    x = _x
  end
  # if x.head ≢ :(->)
  instr = last(x.args).value
  instr ∈ keys(COST) && return Instruction(:LoopVectorization, instr)
  # end
  instr = gensym!(ls, "f")
  pushpreamble!(ls, Expr(:(=), instr, x))
  Instruction(Symbol(""), instr)
end
instruction!(ls::LoopSet, x::Symbol) = instruction(x)
function instruction!(ls::LoopSet, f::F) where {F <: Function}
  get(FUNCTIONSYMBOLS, F) do
    instr = gensym!(ls, "f")
    pushpreamble!(ls, Expr(:(=), instr, f))
    Instruction(Symbol(""), instr)
  end
end

function maybe_const_compute!(ls::LoopSet, LHS::Symbol, op::Operation, elementbytes::Int, position::Int)
  # return op
  if iscompute(op) && iszero(length(loopdependencies(op)))
    ls.opdict[LHS] = add_constant!(ls, LHS, ls.loopsymbols[1:position], gensym!(ls, instruction(op).instr), elementbytes, :numericconstant)
  else
    # op.dependencies = ls.loopsymbols[1:position]
    op
  end
end
strip_op_linenumber_nodes(q::Expr) = only(filter(x -> !isa(x, LineNumberNode), q.args))

function add_operation!(ls::LoopSet, LHS::Symbol, RHS::Symbol, elementbytes::Int, position::Int)
  add_constant!(ls, RHS, ls.loopsymbols[1:position], LHS, elementbytes)
end
function add_comparison!(ls::LoopSet, LHS::Symbol, RHS::Expr, elementbytes::Int, position::Int)
  Nargs = length(RHS.args)
  @assert (Nargs ≥ 5) & isodd(Nargs)
  p1 = add_assignment!(ls, gensym!(ls, "leftcmp"), RHS.args[1], elementbytes, position)::Operation
  p2 = add_assignment!(ls, gensym!(ls, "middlecmp"), RHS.args[3], elementbytes, position)::Operation
  cmpname = Nargs == 3 ? LHS : gensym!(ls, "cmp")
  cmp = add_compute!(ls, cmpname, RHS.args[2], Operation[p1, p2], elementbytes)::Operation
  for i ∈ 5:2:Nargs
    pnew = add_assignment!(ls, gensym!(ls, "rightcmp"), RHS.args[i], elementbytes, position)::Operation
    cmpchain = add_compute!(ls, gensym!(ls, "cmpchain"), RHS.args[i-1], Operation[p2, pnew], elementbytes)::Operation
    cmpname = Nargs == i ? LHS : gensym!(ls, "cmp")
    cmp = add_compute!(ls, cmpname, :&, [cmp, cmpchain], elementbytes)::Operation
    p2 = pnew
  end
  return cmp
end
function add_operation!(
  ls::LoopSet, LHS::Symbol, RHS::Expr, elementbytes::Int, position::Int
)
  if RHS.head === :ref
    add_load_ref!(ls, LHS, RHS, elementbytes)
  elseif RHS.head === :call
    f = first(RHS.args)
    if f === :getindex
      add_load_getindex!(ls, LHS, RHS, elementbytes)
    elseif f isa Symbol && Base.sym_in(f, (:zero, :one, :typemin, :typemax))
      c = gensym!(ls, f)
      op = add_constant!(ls, c, ls.loopsymbols[1:position], LHS, elementbytes, :numericconstant)
      if f === :zero
        push!(ls.preamble_zeros, (identifier(op), IntOrFloat))
      else
        push!(ls.preamble_funcofeltypes, (identifier(op), reduction_zero_class(f)))
      end
      op
    else
      # maybe_const_compute!(ls, add_compute!(ls, LHS, RHS, elementbytes, position), elementbytes, position)
      add_compute!(ls, LHS, RHS, elementbytes, position)
    end
  elseif RHS.head === :if
    add_if!(ls, LHS, RHS, elementbytes, position)
  elseif RHS.head === :block
    add_operation!(ls, LHS, strip_op_linenumber_nodes(RHS), elementbytes, position)
  elseif RHS.head === :(.)
    c = gensym!(ls, "getproperty")
    pushpreamble!(ls, Expr(:(=), c, RHS))
    add_constant!(ls, c, elementbytes)
    # op = add_constant!(ls, c, ls.loopsymbols[1:position], LHS, elementbytes, :numericconstant)
    # pushpreamble!(ls, op, c)
    # op
  elseif Meta.isexpr(RHS, :comparison)
    add_comparison!(ls, LHS, RHS, elementbytes, position)
  else
    throw(LoopError("Expression not recognized.", RHS))
  end
end
add_operation!(ls::LoopSet, RHS::Expr, elementbytes::Int, position::Int) = add_operation!(ls, gensym!(ls, "LHS"), RHS, elementbytes, position)
function add_operation!(
  ls::LoopSet, LHS_sym::Symbol, RHS::Expr, LHS_ref::ArrayReferenceMetaPosition, elementbytes::Int, position::Int
  )
  if RHS.head === :ref# || (RHS.head === :call && first(RHS.args) === :getindex)
    array, rawindices = ref_from_expr!(ls, RHS)
    RHS_ref = array_reference_meta!(ls, array, rawindices, elementbytes, gensym!(ls, LHS_sym))
    op = add_load!(ls, RHS_ref, elementbytes)
    iop = add_compute!(ls, LHS_sym, :identity, [op], elementbytes)
    # pushfirst!(LHS_ref.parents, iop)
  elseif RHS.head === :call
    f = first(RHS.args)
    if f === :getindex
      add_load!(ls, LHS_sym, LHS_ref, elementbytes)
    elseif f isa Symbol && Base.sym_in(f, (:zero, :one, :typemin, :typemax))
      c = gensym!(ls, f)
      op = add_constant!(ls, c, ls.loopsymbols[1:position], LHS_sym, elementbytes, :numericconstant)
      # op = add_constant!(ls, c, Symbol[], LHS_sym, elementbytes, :numericconstant)
      if f === :zero
        push!(ls.preamble_zeros, (identifier(op), IntOrFloat))
      else
        push!(ls.preamble_funcofeltypes, (identifier(op), reduction_zero_class(f)))
      end
      op
    else
      add_compute!(ls, LHS_sym, RHS, elementbytes, position, LHS_ref)
    end
  elseif RHS.head === :if
    add_if!(ls, LHS_sym, RHS, elementbytes, position, LHS_ref)
  elseif RHS.head === :block
    add_operation!(ls, LHS, strip_op_linenumber_nodes(RHS), elementbytes, position)
  elseif RHS.head === :(.)
    c = gensym!(ls, "getproperty")
    pushpreamble!(ls, Expr(:(=), c, RHS))
    add_constant!(ls, c, elementbytes)
    # op = add_constant!(ls, c, ls.loopsymbols[1:position], LHS_sym, elementbytes, :numericconstant)
    # pushpreamble!(ls, op, c)
    # op
  elseif Meta.isexpr(RHS, :comparison, 5)
    add_comparison!(ls, LHS, RHS, elementbytes, position)
  else
    throw(LoopError("Expression not recognized.", RHS))
  end
end

function prepare_rhs_for_storage!(ls::LoopSet, RHS::Union{Symbol,Expr}, array, rawindices, elementbytes::Int, position::Int)::Operation
  RHS isa Symbol && return add_store!(ls, RHS, array, rawindices, elementbytes)
  mpref = array_reference_meta!(ls, array, rawindices, elementbytes)
  cachedparents = copy(mpref.parents)
  ref = mpref.mref.ref
  lrhs = gensym!(ls, "RHS")
  mpref.varname = lrhs
  add_operation!(ls, lrhs, RHS, mpref, elementbytes, position)
  mpref.parents = cachedparents
  op = add_store!(ls, mpref, elementbytes)
  if lrhs ∈ keys(ls.opdict)
    ls.syms_aliasing_refs[findfirst(==(mpref.mref), ls.refs_aliasing_syms)] = lrhs
  end
  return op
end

function add_assignment!(ls::LoopSet, LHS, RHS, elementbytes::Int, position::Int)
  if LHS isa Symbol
    if RHS isa Expr
      maybe_const_compute!(ls, LHS, add_operation!(ls, LHS, RHS, elementbytes, position), elementbytes, position)
    else
      add_constant!(ls, RHS, ls.loopsymbols[1:position], LHS, elementbytes)
    end
  elseif LHS isa Expr
    if LHS.head === :ref
      if RHS isa Symbol
        add_store_ref!(ls, RHS, LHS, elementbytes)
      elseif RHS isa Expr
        # need to check if LHS appears in RHS
        # assign RHS to lrhs
        array, rawindices = ref_from_expr!(ls, LHS)
        prepare_rhs_for_storage!(ls, RHS, array, rawindices, elementbytes, position)
      else
        add_store_ref!(ls, RHS, LHS, elementbytes)  # is this necessary? (Extension API?)
      end
    elseif LHS.head === :tuple
      if RHS.head === :tuple
        for i ∈ eachindex(LHS.args)
          add_assignment!(ls, LHS.args[i], RHS.args[i], elementbytes, position)
        end
        return last(operations(ls)) # FIXME: dummy
      end
      @assert length(LHS.args) ≤ 14 "Functions returning more than 9 values aren't currently supported."
      lhstemp = gensym!(ls, "lhstuple")
      vparents = Operation[maybe_const_compute!(ls, lhstemp, add_operation!(ls, lhstemp, RHS, elementbytes, position), elementbytes, position)]
      for i ∈ eachindex(LHS.args)
        f = (:first,:second,:third,:fourth,:fifth,:sixth,:seventh,:eighth,:ninth,:tenth,:eleventh,:twelfth,:thirteenth,:last)[i]
        lhsi = LHS.args[i]
        if lhsi isa Symbol
          add_compute!(ls, lhsi, f, vparents, elementbytes)
        elseif lhsi isa Expr && lhsi.head === :ref
          tempunpacksym = gensym!(ls, "tempunpack")
          add_compute!(ls, tempunpacksym, f, vparents, elementbytes)
          add_store_ref!(ls, tempunpacksym, lhsi, elementbytes)
        else
          throw(LoopError("Unpacking the above expression in the left hand side was not understood/supported.", lhsi))
        end
      end
      first(vparents)
    else
      throw(LoopError("LHS not understood; only `:ref`s and `:tuple`s are currently supported.", LHS))
    end
  else
    throw(LoopError("LHS not understood.", LHS))
  end
end

function Base.push!(ls::LoopSet, ex::Expr, elementbytes::Int, position::Int, mpref::Union{Nothing,ArrayReferenceMetaPosition} = nothing)
  if ex.head === :call
    finex = first(ex.args)::Symbol
    if finex === :setindex!
      array, rawindices = ref_from_setindex!(ls, ex)
      prepare_rhs_for_storage!(ls, ex.args[3]::Union{Symbol,Expr}, array, rawindices, elementbytes, position)
    else
      error("Function $finex not recognized.")
    end
  elseif ex.head === :(=)
    add_assignment!(ls, ex.args[1], ex.args[2], elementbytes, position)
  elseif ex.head === :block
    add_block!(ls, ex, elementbytes, position)
  elseif ex.head === :for
    add_loop!(ls, ex, elementbytes)
  elseif ex.head === :&&
    add_andblock!(ls, ex, elementbytes, position)
  elseif ex.head === :||
    add_orblock!(ls, ex, elementbytes, position)
  elseif ex.head === :local # Handle locals introduced by `@inbounds`; using `local` with `@turbo` is not recomended (nor is `@inbounds`; which applies automatically regardless)
    @assert length(ex.args) == 1 # TODO replace assert + first with "only" once support for Julia < 1.4 is dropped
    localbody = first(ex.args)
    @assert localbody.head === :(=)
    @assert length(localbody.args) == 2
    LHS = (localbody.args[1])::Symbol
    RHS = push!(ls, (localbody.args[2]), elementbytes, position, mpref)
    if isstore(RHS)
      RHS
    else
      add_compute!(ls, LHS, :identity, [RHS], elementbytes)
    end
  else
    throw(LoopError("Don't know how to handle expression.", ex))
  end
end

function UnrollSpecification(ls::LoopSet, u₁loop::Symbol, u₂loop::Symbol, vloopsym::Symbol, u₁, u₂)
  order = names(ls)
  nu₁ = findfirst(Base.Fix2(===,u₁loop), order)::Int
  nu₂ = u₂ == -1 ? nu₁ : findfirst(Base.Fix2(===,u₂loop), order)::Int
  nv = findfirst(Base.Fix2(===,vloopsym), order)::Int
  UnrollSpecification(nu₁, nu₂, nv, u₁, u₂)
end

"""
    looplengthprod(ls::LoopSet)

  Convert to `Float64` for the sake of non-64 bit platforms.
  """
function looplengthprod(ls::LoopSet)
  l = 1.0
  for loop ∈ ls.loops
    l *= Float64(length(loop))
  end
  l
end
# prod(Float64 ∘ length, ls.loops)

function looplength(ls::LoopSet, s::Symbol)
  # search_tree(parents(operations(ls)[i]), name(op)) && return true
  id = getloopid_or_nothing(ls, s)
  if id === nothing
    l = 0.0
    # TODO: we could double count a loop.
    for op ∈ operations(ls)
      name(op) === s || continue
      for opp ∈ parents(op)
        if isloopvalue(opp)
          oppname = first(loopdependencies(opp))
          l += looplength(ls, oppname)
        elseif iscompute(opp)
          oppname = name(opp)
          l += looplength(ls, oppname)
          # TODO elseif isconstant(opp)
        end
      end
      l += 1 - length(parents(op))
    end
    l
  else
    Float64(length(ls.loops[id]))
  end
end

function accept_reorder_according_to_tracked_reductions(ls::LoopSet, reordered::Symbol)
  for op ∈ operations(ls)
    if reordered ∈ loopdependencies(op)
      for opp ∈ parents(op)
        (iscompute(opp) && isanouterreduction(ls, opp)) && return 0x00
      end
    end
  end
  0x03
end
function check_valid_reorder_dims!(ls::LoopSet)
  validreorder = ls.validreorder
  resize!(validreorder, num_loops(ls))
  fill!(validreorder, 0x03)
  omop = offsetloadcollection(ls)
  @unpack opids = omop
  num_collections = length(opids)
  ops = operations(ls)
  for i ∈ eachindex(opids)
    opidsᵢ = opids[i]
    opi = ops[first(opidsᵢ)]
    opiref = opi.ref.ref
    isl = isload(opi)
    for j ∈ 1:i-1
      opidsⱼ = opids[j]
      opj = ops[first(opidsⱼ)]
      (isl ⊻ isload(opj)) || continue # try and find load/store combo
      opjref = opj.ref.ref
      sameref(opiref, opjref) || continue
      # possible they have shared offsets in certain directions
      for k ∈ eachindex(ls.loops)
        validreorder[k] == 0x03 || continue# if already demoted, don't need to check again
        loopk = ls.loops[k]
        itersym = loopk.itersymbol
        for (l,isym) ∈ enumerate(getindicesonly(opi))
          isym === itersym || continue
          # we match, now we check if this load is offset
          # we'll require all offsets in this index be equal, across both loads and stores
          firstoff = opiref.offsets[l]
          maxdiff = max(checkmismatch(ops, opidsᵢ, l, firstoff, 2:length(opidsᵢ)), checkmismatch(ops, opidsⱼ, l, firstoff, 1:length(opidsⱼ)))
          if maxdiff ≥ (isknown(step(loopk)) ? abs(gethint(step(loopk))) : 1)
            validreorder[k] = 0x00#0x01
          end
        end
      end
    end
  end
  length(ls.outer_reductions) == 0 && return
  for i ∈ eachindex(validreorder)
    validreorder[i] &= accept_reorder_according_to_tracked_reductions(ls, ls.loops[i].itersymbol)
  end
end
function checkmismatch(ops::Vector{Operation}, opids::Vector{Int}, l::Int, firstoff::Int8, checkrange::UnitRange{Int})
  maxabsdiff = 0
  for m ∈ checkrange
    diff = ops[opids[m]].ref.ref.offsets[l] - firstoff
    maxabsdiff = max(maxabsdiff, abs(diff%Int))
  end
  return maxabsdiff
end

offsetloadcollection(ls::LoopSet) = ls.omop
function fill_offset_memop_collection!(ls::LoopSet)
  omop = offsetloadcollection(ls)
  ops = operations(ls)
  num_ops = length(ops)
  @unpack opids, opidcollectionmap, batchedcollections, batchedcollectionmap = omop
  length(opidcollectionmap) == 0 || return
  resize!(opidcollectionmap, num_ops)
  fill!(opidcollectionmap, (0,0));
  resize!(batchedcollectionmap, num_ops)
  fill!(batchedcollectionmap, (0,0))
  empty!(opids);# empty!(offsets);
  for i ∈ 1:num_ops
    op = ops[i]
    isconditionalmemop(op) && continue # not supported yet
    opref = op.ref.ref
    opisload = isload(op)
    (opisload | isstore(op)) || continue
    opidcollectionmap[i] === (0,0) || continue # if not -1, we already handled
    isdiscontiguous(op) && continue
    collectionsize = 0
    for j ∈ i+1:num_ops
      opp = ops[j]
      isconditionalmemop(opp) && continue # not supported yet
      if opisload # Each collection is either entirely loads or entirely stores
        isload(opp) || continue
      else
        isstore(opp) || continue
      end
      oppref = opp.ref.ref
      sameref(opref, oppref) || continue
      if collectionsize == 0
        push!(opids, [identifier(op), identifier(opp)])
        # push!(offsets, [opref.offsets, oppref.offsets])
        opidcollectionmap[identifier(op)] = (length(opids),1)
      else
        push!(last(opids), identifier(opp))
        # push!(last(offsets), oppref.offsets)
      end
      opidcollectionmap[identifier(opp)] = (length(opids),length(last(opids)))
      collectionsize += 1
    end
  end
  for (collectionid,opidc) ∈ enumerate(opids)
    length(opidc) > 1 || continue

    # we check if we can turn the offsets into an unroll
    # we have up to `length(opidc)` loads to do, so we allocate that many "base" vectors
    # then we iterate through them, adding them to collections as appropriate
    # inner vector tuple is of (op_pos_w/in collection,o)
    unroll_collections = Vector{Vector{Tuple{Int,Int}}}(undef, length(opidc))
    num_unroll_collections = 0
    # num_ops_considered = length(opidc)
    r = 2:length(getindices(ops[first(opidc)]))

    for (i,opid) ∈ enumerate(opidc)
      op = ops[opid]
      offset = getoffsets(op)
      o = offset[1]
      v = view(offset, r)
      found_match = false
      for j ∈ 1:num_unroll_collections
        collectionⱼ = unroll_collections[j]
        # giet id (`first`) of first item in collection to get base offsets for comparison
        if view(getoffsets(ops[opidc[first(first(collectionⱼ))]]), r) == v
          found_match = true
          push!(collectionⱼ, (i, o))
        end
      end
      if !found_match
        num_unroll_collections += 1 # the `i` points to position within `opidc`
        unroll_collections[num_unroll_collections] = [(i,o)]
      end
    end
    for j ∈ 1:num_unroll_collections
      collectionⱼ = unroll_collections[j]
      collen = length(collectionⱼ)
      collen ≤ 1 && continue
      # we have multiple, easiest to process if we sort them
      sort!(collectionⱼ, by=last)
      istart = 1; ostart = last(first(collectionⱼ))
      oprev = ostart
      for i ∈ 2:collen
        onext = last(collectionⱼ[i])
        if onext == oprev + 1
          oprev = onext
          continue
        end
        # we skipped one, so we must now lower all previous
        if oprev ≠ ostart # it's just 1
          pushbatchedcollection!(batchedcollections, batchedcollectionmap, opidc, ops, collectionⱼ, istart, i-1)
        end
        # restart istart and ostart
        istart = i
        ostart = onext
        oprev = onext
      end
      if istart ≠ collen
        pushbatchedcollection!(batchedcollections, batchedcollectionmap, opidc, ops, collectionⱼ, istart, collen)
      end
    end
  end
  check_valid_reorder_dims!(ls)
end

function pushbatchedcollection!(batchedcollections, batchedcollectionmap, opidc, ops, collectionⱼ, istart, istop)
  colview = view(collectionⱼ, istart:istop)
  push!(batchedcollections, colview)
  bclen = length(batchedcollections)
  for (i,(k,_)) ∈ enumerate(colview)
    # batchedcollectionmap[identifier(op)] gives index into `batchedcollections` containing `colview`
    batchedcollectionmap[identifier(ops[opidc[k]])] = (bclen,i)
  end
end

"""
  Returns `0` if the op is the declaration of the constant outerreduction variable.
  Returns `n`, where `n` is the constant declarations's index among parents(op), if op is an outter reduction.
  Returns `-1` if not an outerreduction.
  """
function isouterreduction(ls::LoopSet, op::Operation)
  if isconstant(op) # equivalent to checking if length(loopdependencies(op)) == 0
    instr = op.instruction
    instr == LOOPCONSTANT && return Core.ifelse(length(loopdependencies(op)) == 0, 0, -1)
    instr.mod === GLOBALCONSTANT && return -1
    ops = operations(ls)
    for or ∈ ls.outer_reductions
      name(op) === name(ops[or]) && return 0
    end
    -1
  elseif iscompute(op)
    var = op.variable
    for opid ∈ ls.outer_reductions
      rop = operations(ls)[opid]
      if rop === op
        for (n,opp) ∈ enumerate(parents(op))
          opp.variable === var && return n
        end
      else
        for (n,opp) ∈ enumerate(parents(op))
          opp === rop && return n
          search_tree(parents(opp), rop.variable) && return n
        end
      end
    end
    -1
  else
    -1
  end
end

struct LoopError <: Exception
  msg
  ex
  LoopError(msg, ex=nothing) = new(msg, ex)
end

function Base.showerror(io::IO, err::LoopError)
  printstyled(io, err.msg; color = :red)
  err.ex === nothing || printstyled(io, '\n', err.ex)
end


text:
function cc_avg(x::Neuro1DRealSignal; channels::Union{Vector{Int64}, Nothing} = nothing)::Neuro1DRealSignal
    if (channels == nothing)
        return cc_avg(x, channels = [1:1:size(x.signal)[2];])
    end

    Neuro1DRealSignal(
        signal = Statistics.mean(x.signal[:, channels]; dims = 2),
        sample_rate = x.sample_rate
    )
end

ext: jl


text:
# License for module ModiaMath.Logging: MIT
# Copyright 2017-2018, DLR Institute of System Dynamics and Control

"""
    module ModiaMath.Logger

Log model evaluations.

# Main developer
Martin Otter, [DLR - Institute of System Dynamics and Control](https://www.dlr.de/sr/en)
"""
module Logging
@eval using Printf

import ..TypesAndStructs: Logger, SimulationStatistics

export setLog!, setAllLogCategories!, setLogCategories!
export isLogStatistics, isLogProgress, isLogInfos, isLogWarnings, isLogEvents
export logOn!, logOff!, setLogCategories
export reInitializeStatistics!, set_nResultsForSimulationStatistics!

function setLog!(logger::Logger, log::Bool)
    logger.log = log
    return nothing
end

function setAllLogCategories!(logger::Logger; default=true)
    logger.statistics = default
    logger.progress   = default
    logger.infos      = default
    logger.warnings   = default
    logger.events     = default
end

"""
    ModiaMath.setLogCategories!(obj, categories; reinit=true)

Set log categories on obj (of type ModiaMath.SimulationState, ModiaMath.AbstractSimulationModel,
or ModiaMath.Logger) as vector of symbols, e.g. setLogCategories!(simulationModel, [:LogProgess]).
Supported categories:
- `:LogStatistics`, print statistics information at end of simulation
- `:LogProgress`, print progress information during simulation
- `:LogInfos`, print information messages of the model
- `:LogWarnings`, print warning messages of the model
- `:LogEvents`, log events of the model

If option reinit=true, all previous set categories are reinitialized to be no longer present.
If reinit=false, previously set categories are not changed.
"""
function setLogCategories!(logger::Logger, categories::Vector{Symbol}; reinit=true)
    if reinit
        setAllLogCategories!(logger;default=false)
    end

    for c in categories
        if c == :LogStatistics
            logger.statistics = true
        elseif c == :LogProgress
            logger.progress = true
        elseif c == :LogInfos
            logger.infos = true
        elseif c == :LogWarnings
            logger.warnings = true
        elseif c == :LogEvents
            logger.events = true
        else
            warning("Log categorie ", c, " not known (will be ignored)")
        end
    end
    return nothing
end

"""
    ModiaMath.logOn!(obj)

Enable logging on `obj` (of type ModiaMath.SimulationState, ModiaMath.AbstractSimulationModel,
or ModiaMath.Logger)
"""
function logOn!(logger::Logger)
    logger.log = true
    return nothing
end

"""
    ModiaMath.logOff!(obj)

Disable logging on `obj` (of type ModiaMath.SimulationState, ModiaMath.AbstractSimulationModel,
or ModiaMath.Logger)
"""
function logOff!(logger::Logger)
    logger.log = false
    return nothing
end

"""
    ModiaMath.isLogStatistics(logger::ModiaMath.Logger)

Return true, if logger settings require to print **statistics** messages of the model.
"""
isLogStatistics(logger::Logger) = logger.log && logger.statistics

"""
    ModiaMath.isLogProgress(logger::ModiaMath.Logger)

Return true, if logger settings require to print **progress** messages of the model
"""
isLogProgress(logger::Logger) = logger.log && logger.progress

"""
    ModiaMath.isLogInfos(obj)

Return true, if logger settings require to print **info** messages of the model
(obj must be of type ModiaMath.SimulationState, ModiaMath.AbstractSimulationModel,
or ModiaMath.Logger).
"""
isLogInfos(logger::Logger)      = logger.log && logger.infos

"""
    ModiaMath.isLogWarnings(obj)

Return true, if logger settings require to print **warning** messages of the model
(obj must be of type ModiaMath.SimulationState, ModiaMath.AbstractSimulationModel,
or ModiaMath.Logger).
"""
isLogWarnings(logger::Logger)   = logger.log && logger.warnings

"""
    ModiaMath.isLogEvents(obj)

Return true, if logger settings require to print **event** messages of the model
(obj must be of type ModiaMath.SimulationState, ModiaMath.AbstractSimulationModel,
or ModiaMath.Logger).
"""
isLogEvents(logger::Logger)     = logger.log && logger.events

function reInitializeStatistics!(stat::SimulationStatistics,
                                 startTime::Float64, stopTime::Float64, interval::Float64, tolerance::Float64)
    stat.cpuTimeInitialization = 0.0
    stat.cpuTimeIntegration    = 0.0
    stat.startTime      = startTime
    stat.stopTime       = stopTime
    stat.interval       = interval
    stat.tolerance      = tolerance
    stat.nResults       = 0
    stat.nSteps         = 0
    stat.nResidues      = 0
    stat.nZeroCrossings = 0
    stat.nJac           = 0
    stat.nTimeEvents    = 0
    stat.nStateEvents   = 0
    stat.nRestartEvents = 0
    stat.nErrTestFails  = 0
    stat.h0             = floatmax(Float64)
    stat.hMin           = floatmax(Float64)
    stat.hMax           = 0.0
    stat.orderMax       = 0
end

import Base.show
Base.print(io::IO, stat::SimulationStatistics) = show(io, stat)

function Base.show(io::IO, stat::SimulationStatistics)
    println(io, "        structureOfDAE = ", stat.structureOfDAE)
    @printf(io, "        cpuTime        = %.2g s (init: %.2g s, integration: %.2g s)\n",
                                           stat.cpuTimeInitialization + stat.cpuTimeIntegration,
                                           stat.cpuTimeInitialization, stat.cpuTimeIntegration)
    println(io, "        startTime      = ", stat.startTime, " s")
    println(io, "        stopTime       = ", stat.stopTime, " s")
    println(io, "        interval       = ", stat.interval, " s")
    println(io, "        tolerance      = ", stat.tolerance)
    println(io, "        nEquations     = ", stat.nEquations, typeof(stat.nConstraints)!=Missing ?
                                                                  " (includes " * string(stat.nConstraints) * " constraints)" : "" )
    println(io, "        nResults       = ", stat.nResults)
    println(io, "        nSteps         = ", stat.nSteps)
    println(io, "        nResidues      = ", stat.nResidues, " (includes residue calls for Jacobian)")
    println(io, "        nZeroCrossings = ", stat.nZeroCrossings)
    println(io, "        nJac           = ", stat.nJac)
    println(io, "        nTimeEvents    = ", stat.nTimeEvents)
    println(io, "        nStateEvents   = ", stat.nStateEvents)
    println(io, "        nRestartEvents = ", stat.nRestartEvents)
    println(io, "        nErrTestFails  = ", stat.nErrTestFails)
    @printf(io, "        h0             = %.2g s\n", stat.h0)
    @printf(io, "        hMin           = %.2g s\n", stat.hMin)
    @printf(io, "        hMax           = %.2g s\n", stat.hMax)
    println(io, "        orderMax       = ", stat.orderMax)
    println(io, "        sparseSolver   = ", stat.sparseSolver)

    if stat.sparseSolver
        println(io, "        nGroups        = ", stat.nGroups)
    end
end

function set_nResultsForSimulationStatistics!(stat::SimulationStatistics, nt::Int)
    stat.nResults = nt
    return nothing
end

end


text:
const _allAxes = [:auto, :left, :right]
@compat const _axesAliases = KW(
    :a => :auto,
    :l => :left,
    :r => :right
  )

const _3dTypes = [:path3d, :scatter3d, :surface, :wireframe, :contour3d]
const _allTypes = vcat([
                        :none, :line, :path, :steppre, :steppost, :sticks, :scatter,
                        :heatmap, :hexbin, :hist, :hist2d, :hist3d, :density, :bar, :hline, :vline, :ohlc,
                        :contour, :pie, :shape, :box, :violin, :quiver, :image
                       ], _3dTypes)
@compat const _typeAliases = KW(
    :n             => :none,
    :no            => :none,
    :l             => :line,
    :p             => :path,
    :stepinv       => :steppre,
    :stepsinv      => :steppre,
    :stepinverted  => :steppre,
    :stepsinverted => :steppre,
    :step          => :steppost,
    :steps         => :steppost,
    :stair         => :steppost,
    :stairs        => :steppost,
    :stem          => :sticks,
    :stems         => :sticks,
    :dots          => :scatter,
    :histogram     => :hist,
    :pdf           => :density,
    :contours      => :contour,
    :line3d        => :path3d,
    :surf          => :surface,
    :wire          => :wireframe,
    :shapes        => :shape,
    :poly          => :shape,
    :polygon       => :shape,
    :boxplot       => :box,
    :velocity      => :quiver,
    :gradient      => :quiver,
    :img           => :image,
    :imshow        => :image,
    :imagesc       => :image,
  )

like_histogram(linetype::Symbol) = linetype in (:hist, :density)
like_line(linetype::Symbol)      = linetype in (:line, :path, :steppre, :steppost)
like_surface(linetype::Symbol)   = linetype in (:contour, :contour3d, :heatmap, :surface, :wireframe, :image)

is3d(linetype::Symbol) = linetype in _3dTypes
is3d(d::KW) = trueOrAllTrue(is3d, d[:linetype])

const _allStyles = [:auto, :solid, :dash, :dot, :dashdot, :dashdotdot]
@compat const _styleAliases = KW(
    :a    => :auto,
    :s    => :solid,
    :d    => :dash,
    :dd   => :dashdot,
    :ddd  => :dashdotdot,
  )

# const _allMarkers = [:none, :auto, :ellipse, :rect, :diamond, :utriangle, :dtriangle,
#                      :cross, :xcross, :star5, :star8, :hexagon, :octagon, Shape]
const _allMarkers = vcat(:none, :auto, sort(collect(keys(_shapes))))
@compat const _markerAliases = KW(
    :n            => :none,
    :no           => :none,
    :a            => :auto,
    :circle       => :ellipse,
    :c            => :ellipse,
    :square       => :rect,
    :sq           => :rect,
    :r            => :rect,
    :d            => :diamond,
    :^            => :utriangle,
    :ut           => :utriangle,
    :utri         => :utriangle,
    :uptri        => :utriangle,
    :uptriangle   => :utriangle,
    :v            => :dtriangle,
    :V            => :dtriangle,
    :dt           => :dtriangle,
    :dtri         => :dtriangle,
    :downtri      => :dtriangle,
    :downtriangle => :dtriangle,
    :+            => :cross,
    :plus         => :cross,
    :x            => :xcross,
    :X            => :xcross,
    :star         => :star5,
    :s            => :star5,
    :star1        => :star5,
    :s2           => :star8,
    :star2        => :star8,
    :p            => :pentagon,
    :pent         => :pentagon,
    :h            => :hexagon,
    :hex          => :hexagon,
    :hep          => :heptagon,
    :o            => :octagon,
    :oct          => :octagon,
    :spike        => :vline,
  )

const _allScales = [:identity, :ln, :log2, :log10, :asinh, :sqrt]
@compat const _scaleAliases = KW(
    :none => :identity,
    :log  => :log10,
  )

# -----------------------------------------------------------------------------

const _seriesDefaults = KW()

# series-specific
_seriesDefaults[:axis]            = :left
_seriesDefaults[:label]           = "AUTO"
_seriesDefaults[:seriescolor]     = :auto
_seriesDefaults[:seriesalpha]     = nothing
_seriesDefaults[:linetype]        = :path
_seriesDefaults[:linestyle]       = :solid
_seriesDefaults[:linewidth]       = :auto
_seriesDefaults[:linecolor]       = :match
_seriesDefaults[:linealpha]       = nothing
_seriesDefaults[:fillrange]       = nothing   # ribbons, areas, etc
_seriesDefaults[:fillcolor]       = :match
_seriesDefaults[:fillalpha]       = nothing
_seriesDefaults[:markershape]     = :none
_seriesDefaults[:markercolor]     = :match
_seriesDefaults[:markeralpha]     = nothing
_seriesDefaults[:markersize]      = 6
_seriesDefaults[:markerstrokestyle] = :solid
_seriesDefaults[:markerstrokewidth] = 1
_seriesDefaults[:markerstrokecolor] = :match
_seriesDefaults[:markerstrokealpha] = nothing
_seriesDefaults[:bins]            = 30               # number of bins for hists
_seriesDefaults[:smooth]          = false               # regression line?
_seriesDefaults[:group]           = nothing           # groupby vector
_seriesDefaults[:x]               = nothing
_seriesDefaults[:y]               = nothing
_seriesDefaults[:z]               = nothing           # depth for contour, surface, etc
_seriesDefaults[:marker_z]        = nothing           # value for color scale
_seriesDefaults[:levels]          = 15
_seriesDefaults[:orientation]     = :vertical
_seriesDefaults[:bar_position]    = :overlay  # for bar plots and histograms: could also be stack (stack up) or dodge (side by side)
_seriesDefaults[:xerror]          = nothing
_seriesDefaults[:yerror]          = nothing
_seriesDefaults[:ribbon]          = nothing
_seriesDefaults[:quiver]          = nothing
_seriesDefaults[:normalize]       = false     # do we want a normalized histogram?
_seriesDefaults[:weights]         = nothing   # optional weights for histograms (1D and 2D)
_seriesDefaults[:contours]        = false     # add contours to 3d surface and wireframe plots
_seriesDefaults[:match_dimensions] = false   # do rows match x (true) or y (false) for heatmap/image/spy? see issue 196

const _plotDefaults = KW()

# plot globals
_plotDefaults[:title]             = ""
_plotDefaults[:xlabel]            = ""
_plotDefaults[:ylabel]            = ""
_plotDefaults[:zlabel]            = ""
_plotDefaults[:yrightlabel]       = ""
_plotDefaults[:legend]            = :best
_plotDefaults[:colorbar]          = :legend
_plotDefaults[:background_color]            = colorant"white"   # default for all backgrounds
_plotDefaults[:background_color_legend]     = :match            # background of legend
_plotDefaults[:background_color_inside]     = :match            # background inside grid
_plotDefaults[:background_color_outside]    = :match            # background outside grid
_plotDefaults[:foreground_color]            = :auto             # default for all foregrounds
_plotDefaults[:foreground_color_legend]     = :match            # foreground of legend
_plotDefaults[:foreground_color_grid]       = :match            # grid color
_plotDefaults[:foreground_color_axis]       = :match            # axis border/tick colors
_plotDefaults[:foreground_color_border]     = :match            # plot area border/spines
_plotDefaults[:foreground_color_text]       = :match            # tick text color
_plotDefaults[:foreground_color_guide]      = :match            # guide text color
_plotDefaults[:xlims]             = :auto
_plotDefaults[:ylims]             = :auto
_plotDefaults[:zlims]             = :auto
_plotDefaults[:xticks]            = :auto
_plotDefaults[:yticks]            = :auto
_plotDefaults[:zticks]            = :auto
_plotDefaults[:xscale]            = :identity
_plotDefaults[:yscale]            = :identity
_plotDefaults[:zscale]            = :identity
_plotDefaults[:xrotation]         = 0
_plotDefaults[:yrotation]         = 0
_plotDefaults[:zrotation]         = 0
_plotDefaults[:xflip]             = false
_plotDefaults[:yflip]             = false
_plotDefaults[:zflip]             = false
_plotDefaults[:size]              = (600,400)
_plotDefaults[:pos]               = (0,0)
_plotDefaults[:windowtitle]       = "Plots.jl"
_plotDefaults[:show]              = false
_plotDefaults[:layout]            = nothing
_plotDefaults[:n]                 = -1
_plotDefaults[:nr]                = -1
_plotDefaults[:nc]                = -1
_plotDefaults[:color_palette]     = :auto
_plotDefaults[:link]              = false
_plotDefaults[:linkx]             = false
_plotDefaults[:linky]             = false
_plotDefaults[:linkfunc]          = nothing
_plotDefaults[:tickfont]          = font(8)
_plotDefaults[:guidefont]         = font(11)
_plotDefaults[:legendfont]        = font(8)
_plotDefaults[:grid]              = true
_plotDefaults[:annotation]        = nothing           # annotation tuple(s)... (x,y,annotation)
_plotDefaults[:overwrite_figure]  = false
_plotDefaults[:polar]             = false
_plotDefaults[:aspect_ratio]        = :none             # choose from :none or :equal

# TODO: x/y scales

const _allArgs = sort(collect(union(keys(_seriesDefaults), keys(_plotDefaults))))
supportedArgs(::AbstractBackend) = error("supportedArgs not defined") #_allArgs
supportedArgs() = supportedArgs(backend())

RecipesBase.is_key_supported(k::Symbol) = (k in supportedArgs())

# -----------------------------------------------------------------------------

makeplural(s::Symbol) = symbol(string(s,"s"))

autopick(arr::AVec, idx::Integer) = arr[mod1(idx,length(arr))]
autopick(notarr, idx::Integer) = notarr

autopick_ignore_none_auto(arr::AVec, idx::Integer) = autopick(setdiff(arr, [:none, :auto]), idx)
autopick_ignore_none_auto(notarr, idx::Integer) = notarr

function aliasesAndAutopick(d::KW, sym::Symbol, aliases::KW, options::AVec, plotIndex::Int)
    if d[sym] == :auto
        d[sym] = autopick_ignore_none_auto(options, plotIndex)
    elseif haskey(aliases, d[sym])
        d[sym] = aliases[d[sym]]
    end
end

function aliases(aliasMap::KW, val)
    sortedkeys(filter((k,v)-> v==val, aliasMap))
end

# -----------------------------------------------------------------------------

const _keyAliases = KW()

function add_aliases(sym::Symbol, aliases::Symbol...)
    for alias in aliases
        if haskey(_keyAliases, alias)
            error("Already an alias $alias => $(_keyAliases[alias])... can't also alias $sym")
        end
        _keyAliases[alias] = sym
    end
end

# colors
add_aliases(:seriescolor, :c, :color, :colour)
add_aliases(:linecolor, :lc, :lcolor, :lcolour, :linecolour)
add_aliases(:markercolor, :mc, :mcolor, :mcolour, :markercolour)
add_aliases(:markerstokecolor, :msc, :mscolor, :mscolour, :markerstokecolour)
add_aliases(:fillcolor, :fc, :fcolor, :fcolour, :fillcolour)

add_aliases(:background_color, :bg, :bgcolor, :bg_color, :background,
                              :background_colour, :bgcolour, :bg_colour)
add_aliases(:background_color_legend, :bg_legend, :bglegend, :bgcolor_legend, :bg_color_legend, :background_legend,
                              :background_colour_legend, :bgcolour_legend, :bg_colour_legend)
add_aliases(:background_color_inside, :bg_inside, :bginside, :bgcolor_inside, :bg_color_inside, :background_inside,
                              :background_colour_inside, :bgcolour_inside, :bg_colour_inside)
add_aliases(:background_color_outside, :bg_outside, :bgoutside, :bgcolor_outside, :bg_color_outside, :background_outside,
                              :background_colour_outside, :bgcolour_outside, :bg_colour_outside)
add_aliases(:foreground_color, :fg, :fgcolor, :fg_color, :foreground,
                            :foreground_colour, :fgcolour, :fg_colour)
add_aliases(:foreground_color_legend, :fg_legend, :fglegend, :fgcolor_legend, :fg_color_legend, :foreground_legend,
                            :foreground_colour_legend, :fgcolour_legend, :fg_colour_legend)
add_aliases(:foreground_color_grid, :fg_grid, :fggrid, :fgcolor_grid, :fg_color_grid, :foreground_grid,
                            :foreground_colour_grid, :fgcolour_grid, :fg_colour_grid, :gridcolor)
add_aliases(:foreground_color_axis, :fg_axis, :fgaxis, :fgcolor_axis, :fg_color_axis, :foreground_axis,
                            :foreground_colour_axis, :fgcolour_axis, :fg_colour_axis, :axiscolor)
add_aliases(:foreground_color_border, :fg_border, :fgborder, :fgcolor_border, :fg_color_border, :foreground_border,
                            :foreground_colour_border, :fgcolour_border, :fg_colour_border, :bordercolor, :border)
add_aliases(:foreground_color_text, :fg_text, :fgtext, :fgcolor_text, :fg_color_text, :foreground_text,
                            :foreground_colour_text, :fgcolour_text, :fg_colour_text, :textcolor)
add_aliases(:foreground_color_guide, :fg_guide, :fgguide, :fgcolor_guide, :fg_color_guide, :foreground_guide,
                            :foreground_colour_guide, :fgcolour_guide, :fg_colour_guide, :guidecolor)

# alphas
add_aliases(:seriesalpha, :alpha, :α, :opacity)
add_aliases(:linealpha, :la, :lalpha, :lα, :lineopacity, :lopacity)
add_aliases(:makeralpha, :ma, :malpha, :mα, :makeropacity, :mopacity)
add_aliases(:markerstrokealpha, :msa, :msalpha, :msα, :markerstrokeopacity, :msopacity)
add_aliases(:fillalpha, :fa, :falpha, :fα, :fillopacity, :fopacity)

add_aliases(:label, :lab)
add_aliases(:line, :l)
add_aliases(:linewidth, :w, :width, :lw)
add_aliases(:linetype, :lt, :t, :seriestype)
add_aliases(:linestyle, :style, :s, :ls)
add_aliases(:marker, :m, :mark)
add_aliases(:markershape, :shape)
add_aliases(:markersize, :ms, :msize)
add_aliases(:marker_z, :markerz, :zcolor)
add_aliases(:fill, :f, :area)
add_aliases(:fillrange, :fillrng, :frange, :fillto, :fill_between)
add_aliases(:group, :g, :grouping)
add_aliases(:bins, :bin, :nbin, :nbins, :nb)
add_aliases(:ribbon, :rib)
add_aliases(:annotation, :ann, :anns, :annotate, :annotations)
add_aliases(:xlabel, :xlab, :xl)
add_aliases(:xlims, :xlim, :xlimit, :xlimits)
add_aliases(:xticks, :xtick)
add_aliases(:xrotation, :xrot, :xr)
add_aliases(:ylabel, :ylab, :yl)
add_aliases(:ylims, :ylim, :ylimit, :ylimits)
add_aliases(:yticks, :ytick)
add_aliases(:yrightlabel, :yrlab, :yrl, :ylabel2, :y2label, :ylab2, :y2lab, :ylabr, :ylabelright)
add_aliases(:yrightlims, :yrlim, :yrlimit, :yrlimits)
add_aliases(:yrightticks, :yrtick)
add_aliases(:yrotation, :yrot, :yr)
add_aliases(:zlabel, :zlab, :zl)
add_aliases(:zlims, :zlim, :zlimit, :zlimits)
add_aliases(:zticks, :ztick)
add_aliases(:zrotation, :zrot, :zr)
add_aliases(:legend, :leg, :key)
add_aliases(:colorbar, :cb, :cbar, :colorkey)
add_aliases(:smooth, :regression, :reg)
add_aliases(:levels, :nlevels, :nlev, :levs)
add_aliases(:size, :windowsize, :wsize)
add_aliases(:windowtitle, :wtitle)
add_aliases(:show, :gui, :display)
add_aliases(:color_palette, :palette)
add_aliases(:linkx, :xlink)
add_aliases(:linky, :ylink)
add_aliases(:nr, :nrow, :nrows, :rows)
add_aliases(:nc, :ncol, :ncols, :cols, :ncolumns, :columns)
add_aliases(:overwrite_figure, :clf, :clearfig, :overwrite, :reuse)
add_aliases(:xerror, :xerr, :xerrorbar)
add_aliases(:yerror, :yerr, :yerrorbar, :err, :errorbar)
add_aliases(:quiver, :velocity, :quiver2d, :gradient)
add_aliases(:normalize, :norm, :normed, :normalized)
add_aliases(:aspect_ratio, :aspectratio, :axis_ratio, :axisratio, :ratio)

# add all pluralized forms to the _keyAliases dict
for arg in keys(_seriesDefaults)
    _keyAliases[makeplural(arg)] = arg
end

# -----------------------------------------------------------------------------

# update the defaults globally

"""
`default(key)` returns the current default value for that key
`default(key, value)` sets the current default value for that key
`default(; kw...)` will set the current default value for each key/value pair
"""

function default(k::Symbol)
    k = get(_keyAliases, k, k)
    if haskey(_seriesDefaults, k)
        return _seriesDefaults[k]
    elseif haskey(_plotDefaults, k)
        return _plotDefaults[k]
    else
        error("Unknown key: ", k)
    end
end

function default(k::Symbol, v)
    k = get(_keyAliases, k, k)
    if haskey(_seriesDefaults, k)
        _seriesDefaults[k] = v
    elseif haskey(_plotDefaults, k)
        _plotDefaults[k] = v
    else
        error("Unknown key: ", k)
    end
end

function default(; kw...)
    for (k,v) in kw
        default(k, v)
    end
end

# -----------------------------------------------------------------------------

# if arg is a valid color value, then set d[csym] and return true
function handleColors!(d::KW, arg, csym::Symbol)
    try
        if arg == :auto
            d[csym] = :auto
        else
            c = colorscheme(arg)
            d[csym] = c
        end
        return true
    end
    false
end

# given one value (:log, or :flip, or (-1,1), etc), set the appropriate arg
# TODO: use trueOrAllTrue for subplots which can pass vectors for these
function processAxisArg(d::KW, letter::AbstractString, arg)
    T = typeof(arg)
    arg = get(_scaleAliases, arg, arg)
    scale, flip, label, lim, tick = axis_symbols(letter, "scale", "flip", "label", "lims", "ticks")

    if typeof(arg) <: Font
        d[:tickfont] = arg

    elseif arg in _allScales
        d[scale] = arg

    elseif arg in (:flip, :invert, :inverted)
        d[flip] = true

    elseif T <: @compat(AbstractString)
        d[label] = arg

    # xlims/ylims
    elseif (T <: Tuple || T <: AVec) && length(arg) == 2
        d[typeof(arg[1]) <: Number ? lim : tick] = arg

    # xticks/yticks
    elseif T <: AVec
        d[tick] = arg

    elseif arg == nothing
        d[tick] = []

    else
        warn("Skipped $(letter)axis arg $arg")

    end
end

function processLineArg(d::KW, arg)
    # linetype
    if allLineTypes(arg)
        d[:linetype] = arg

    # linestyle
    elseif allStyles(arg)
        d[:linestyle] = arg

    elseif typeof(arg) <: Stroke
        arg.width == nothing || (d[:linewidth] = arg.width)
        arg.color == nothing || (d[:linecolor] = arg.color == :auto ? :auto : colorscheme(arg.color))
        arg.alpha == nothing || (d[:linealpha] = arg.alpha)
        arg.style == nothing || (d[:linestyle] = arg.style)

    elseif typeof(arg) <: Brush
        arg.size  == nothing || (d[:fillrange] = arg.size)
        arg.color == nothing || (d[:fillcolor] = arg.color == :auto ? :auto : colorscheme(arg.color))
        arg.alpha == nothing || (d[:fillalpha] = arg.alpha)

    # linealpha
    elseif allAlphas(arg)
        d[:linealpha] = arg

    # linewidth
    elseif allReals(arg)
        d[:linewidth] = arg

    # color
    elseif !handleColors!(d, arg, :linecolor)
        warn("Skipped line arg $arg.")

    end
end

function processMarkerArg(d::KW, arg)
    # markershape
    if allShapes(arg)
        d[:markershape] = arg

    # stroke style
    elseif allStyles(arg)
        d[:markerstrokestyle] = arg

    elseif typeof(arg) <: Stroke
        arg.width == nothing || (d[:markerstrokewidth] = arg.width)
        arg.color == nothing || (d[:markerstrokecolor] = arg.color == :auto ? :auto : colorscheme(arg.color))
        arg.alpha == nothing || (d[:markerstrokealpha] = arg.alpha)
        arg.style == nothing || (d[:markerstrokestyle] = arg.style)

    elseif typeof(arg) <: Brush
        arg.size  == nothing || (d[:markersize]  = arg.size)
        arg.color == nothing || (d[:markercolor] = arg.color == :auto ? :auto : colorscheme(arg.color))
        arg.alpha == nothing || (d[:markeralpha] = arg.alpha)

    # linealpha
    elseif allAlphas(arg)
        d[:markeralpha] = arg

    # markersize
    elseif allReals(arg)
        d[:markersize] = arg

    # markercolor
    elseif !handleColors!(d, arg, :markercolor)
        warn("Skipped marker arg $arg.")

    end
end

function processFillArg(d::KW, arg)
    if typeof(arg) <: Brush
        arg.size  == nothing || (d[:fillrange] = arg.size)
        arg.color == nothing || (d[:fillcolor] = arg.color == :auto ? :auto : colorscheme(arg.color))
        arg.alpha == nothing || (d[:fillalpha] = arg.alpha)

    # fillrange function
    elseif allFunctions(arg)
        d[:fillrange] = arg

    # fillalpha
    elseif allAlphas(arg)
        d[:fillalpha] = arg

    elseif !handleColors!(d, arg, :fillcolor)

        d[:fillrange] = arg
    end
end

_replace_markershape(shape::Symbol) = get(_markerAliases, shape, shape)
_replace_markershape(shapes::AVec) = map(_replace_markershape, shapes)
_replace_markershape(shape) = shape

function _add_markershape(d::KW)
    # add the markershape if it needs to be added... hack to allow "m=10" to add a shape,
    # and still allow overriding in _apply_recipe
    ms = pop!(d, :markershape_to_add, :none)
    if !haskey(d, :markershape) && ms != :none
        d[:markershape] = ms
    end
end

"Handle all preprocessing of args... break out colors/sizes/etc and replace aliases."
function preprocessArgs!(d::KW)
    replaceAliases!(d, _keyAliases)

    # handle axis args
    for letter in ("x", "y", "z")
        asym = symbol(letter * "axis")
        for arg in wraptuple(get(d, asym, ()))
            processAxisArg(d, letter, arg)
        end
        delete!(d, asym)

        # turn :labels into :ticks_and_labels
        tsym = symbol(letter * "ticks")
        if haskey(d, tsym) && ticksType(d[tsym]) == :labels
            d[tsym] = (1:length(d[tsym]), d[tsym])
        end

        ssym = symbol(letter * "scale")
        if haskey(d, ssym) && haskey(_scaleAliases, d[ssym])
            d[ssym] = _scaleAliases[d[ssym]]
        end
    end

    # handle line args
    for arg in wraptuple(pop!(d, :line, ()))
        processLineArg(d, arg)
    end

    # handle marker args... default to ellipse if shape not set
    anymarker = false
    for arg in wraptuple(get(d, :marker, ()))
        processMarkerArg(d, arg)
        anymarker = true
    end
    delete!(d, :marker)
    if haskey(d, :markershape)
        d[:markershape] = _replace_markershape(d[:markershape])
    elseif anymarker
        d[:markershape_to_add] = :ellipse  # add it after _apply_recipe
    end

    # handle fill
    for arg in wraptuple(get(d, :fill, ()))
        processFillArg(d, arg)
    end
    delete!(d, :fill)

  # convert into strokes and brushes

    # legends
    if haskey(d, :legend)
        d[:legend] = convertLegendValue(d[:legend])
    end
    if haskey(d, :colorbar)
        d[:colorbar] = convertLegendValue(d[:colorbar])
    end

    # handle subplot links
    if haskey(d, :link)
        l = d[:link]
        if isa(l, Bool)
            d[:linkx] = l
            d[:linky] = l
        elseif isa(l, Function)
            d[:linkx] = true
            d[:linky] = true
            d[:linkfunc] = l
        else
            warn("Unhandled/invalid link $l.  Should be a Bool or a function mapping (row,column) -> (linkx, linky), where linkx/y can be Bool or Void (nothing)")
        end
        delete!(d, :link)
    end

    # pull out invalid keywords into their own KW dict... these are likely user-defined through recipes
    kw = KW()
    for k in keys(d)
        try
            # this should error for invalid keywords (assume they are user-defined)
            k == :markershape_to_add || default(k)
        catch
            # not a valid key... pop and add to user list
            kw[k] = pop!(d, k)
        end
    end
    kw
end

# -----------------------------------------------------------------------------

"A special type that will break up incoming data into groups, and allow for easier creation of grouped plots"
type GroupBy
    groupLabels::Vector{UTF8String}   # length == numGroups
    groupIds::Vector{Vector{Int}}     # list of indices for each group
end

# this is when given a vector-type of values to group by
function extractGroupArgs(v::AVec, args...)
    groupLabels = sort(collect(unique(v)))
    n = length(groupLabels)
    if n > 20
        warn("You created n=$n groups... Is that intended?")
    end
    groupIds = Vector{Int}[filter(i -> v[i] == glab, 1:length(v)) for glab in groupLabels]
    GroupBy(map(string, groupLabels), groupIds)
end

# expecting a mapping of "group label" to "group indices"
function extractGroupArgs{T, V<:AVec{Int}}(idxmap::Dict{T,V}, args...)
    groupLabels = sortedkeys(idxmap)
    groupIds = VecI[collect(idxmap[k]) for k in groupLabels]
    GroupBy(groupLabels, groupIds)
end

# -----------------------------------------------------------------------------

function warnOnUnsupportedArgs(pkg::AbstractBackend, d::KW)
    for k in sortedkeys(d)
        if (!(k in supportedArgs(pkg))
                && k != :subplot
                && d[k] != default(k))
            warn("Keyword argument $k not supported with $pkg.  Choose from: $(supportedArgs(pkg))")
        end
    end
end

_markershape_supported(pkg::AbstractBackend, shape::Symbol) = shape in supportedMarkers(pkg)
_markershape_supported(pkg::AbstractBackend, shape::Shape) = Shape in supportedMarkers(pkg)
_markershape_supported(pkg::AbstractBackend, shapes::AVec) = all([_markershape_supported(pkg, shape) for shape in shapes])

function warnOnUnsupported(pkg::AbstractBackend, d::KW)
    (d[:axis] in supportedAxes(pkg)
        || warn("axis $(d[:axis]) is unsupported with $pkg.  Choose from: $(supportedAxes(pkg))"))
    (d[:linetype] == :none
        || d[:linetype] in supportedTypes(pkg)
        || warn("linetype $(d[:linetype]) is unsupported with $pkg.  Choose from: $(supportedTypes(pkg))"))
    (d[:linestyle] in supportedStyles(pkg)
        || warn("linestyle $(d[:linestyle]) is unsupported with $pkg.  Choose from: $(supportedStyles(pkg))"))
    (d[:markershape] == :none
        || _markershape_supported(pkg, d[:markershape])
        || warn("markershape $(d[:markershape]) is unsupported with $pkg.  Choose from: $(supportedMarkers(pkg))"))
end

function warnOnUnsupportedScales(pkg::AbstractBackend, d::KW)
  for k in (:xscale, :yscale)
    if haskey(d, k)
      d[k] in supportedScales(pkg) || warn("scale $(d[k]) is unsupported with $pkg.  Choose from: $(supportedScales(pkg))")
    end
  end
end

# -----------------------------------------------------------------------------

# 1-row matrices will give an element
# multi-row matrices will give a column
# InputWrapper just gives the contents
# anything else is returned as-is
# getArgValue(v::Tuple, idx::Int) = v[mod1(idx, length(v))]
function getArgValue(v::AMat, idx::Int)
    c = mod1(idx, size(v,2))
    size(v,1) == 1 ? v[1,c] : v[:,c]
end
getArgValue(wrapper::InputWrapper, idx) = wrapper.obj
getArgValue(v, idx) = v

# given an argument key (k), we want to extract the argument value for this index.
# if nothing is set (or container is empty), return the default.
function setDictValue(d_in::KW, d_out::KW, k::Symbol, idx::Int, defaults::KW)
    if haskey(d_in, k) && !(typeof(d_in[k]) <: Union{AbstractArray, Tuple} && isempty(d_in[k]))
        d_out[k] = getArgValue(d_in[k], idx)
    else
        d_out[k] = defaults[k]
    end
end

function convertLegendValue(val::Symbol)
    if val in (:both, :all, :yes)
        :best
    elseif val in (:no, :none)
        :none
    elseif val in (:right, :left, :top, :bottom, :inside, :best, :legend)
        val
    else
        error("Invalid symbol for legend: $val")
    end
end
convertLegendValue(val::Bool) = val ? :best : :none

# -----------------------------------------------------------------------------

# build the argument dictionary for the plot
function getPlotArgs(pkg::AbstractBackend, kw, idx::Int; set_defaults = true)
    kwdict = KW(kw)
    d = KW()

    # add defaults?
    if set_defaults
        for k in keys(_plotDefaults)
            setDictValue(kwdict, d, k, idx, _plotDefaults)
        end
    end
    #
    # for k in (:xscale, :yscale)
    #     if haskey(_scaleAliases, d[k])
    #         d[k] = _scaleAliases[d[k]]
    #     end
    # end

    # handle legend/colorbar
    d[:legend] = convertLegendValue(d[:legend])
    d[:colorbar] = convertLegendValue(d[:colorbar])
    if d[:colorbar] == :legend
        d[:colorbar] = d[:legend]
    end

    # convert color
    handlePlotColors(pkg, d)

    # no need for these
    delete!(d, :x)
    delete!(d, :y)

    d
end

function has_black_border_for_default(lt::Symbol)
    like_histogram(lt) || lt in (:hexbin, :bar)
end

# build the argument dictionary for a series
function getSeriesArgs(pkg::AbstractBackend, plotargs::KW, kw, commandIndex::Int, plotIndex::Int, globalIndex::Int)  # TODO, pass in plotargs, not plt
    kwdict = KW(kw)
    d = KW()

    # add defaults?
    for k in keys(_seriesDefaults)
        setDictValue(kwdict, d, k, commandIndex, _seriesDefaults)
    end

    # groupby args?
    for k in (:idxfilter, :numUncounted, :dataframe)
        if haskey(kwdict, k)
            d[k] = kwdict[k]
        end
    end

    if haskey(_typeAliases, d[:linetype])
        d[:linetype] = _typeAliases[d[:linetype]]
    end

    aliasesAndAutopick(d, :axis, _axesAliases, supportedAxes(pkg), plotIndex)
    aliasesAndAutopick(d, :linestyle, _styleAliases, supportedStyles(pkg), plotIndex)
    aliasesAndAutopick(d, :markershape, _markerAliases, supportedMarkers(pkg), plotIndex)

    # update color
    d[:seriescolor] = getSeriesRGBColor(d[:seriescolor], plotargs, plotIndex)

    # # update linecolor
    # c = d[:linecolor]
    # c = (c == :match ? d[:seriescolor] : getSeriesRGBColor(c, plotargs, plotIndex))
    # d[:linecolor] = c

    # # update markercolor
    # c = d[:markercolor]
    # c = (c == :match ? d[:seriescolor] : getSeriesRGBColor(c, plotargs, plotIndex))
    # d[:markercolor] = c

    # # update fillcolor
    # c = d[:fillcolor]
    # c = (c == :match ? d[:seriescolor] : getSeriesRGBColor(c, plotargs, plotIndex))
    # d[:fillcolor] = c

    # update colors
    for csym in (:linecolor, :markercolor, :fillcolor)
        d[csym] = if d[csym] == :match
            if has_black_border_for_default(d[:linetype]) && csym == :linecolor
                :black
            else
                d[:seriescolor]
            end
        else
            getSeriesRGBColor(d[csym], plotargs, plotIndex)
        end
    end

    # update markerstrokecolor
    c = d[:markerstrokecolor]
    c = (c == :match ? plotargs[:foreground_color] : getSeriesRGBColor(c, plotargs, plotIndex))
    d[:markerstrokecolor] = c

    # update alphas
    for asym in (:linealpha, :markeralpha, :markerstrokealpha, :fillalpha)
        if d[asym] == nothing
            d[asym] = d[:seriesalpha]
        end
    end

    # scatter plots don't have a line, but must have a shape
    if d[:linetype] in (:scatter, :scatter3d)
        d[:linewidth] = 0
        if d[:markershape] == :none
            d[:markershape] = :ellipse
        end
    end

    # set label
    label = d[:label]
    label = (label == "AUTO" ? "y$globalIndex" : label)
    if d[:axis] == :right && !(length(label) >= 4 && label[end-3:end] != " (R)")
        label = string(label, " (R)")
    end
    d[:label] = label

    warnOnUnsupported(pkg, d)

    d
end


text:
prunes = [false, false, false, true, true]
L1arr = [false, true, false, true, false]
constant_vel = [false, true, true, true, true]  # First is just stack avg
stackavgarr = .!constant_vel

for (doprune, isl1, constvel, stackavg) in zip(prunes, L1arr, constant_vel, stackavgarr)
    soltype = isl1 ? "l1" : (stackavg ? "stackavg" : "l2")
    prunestr = doprune ? "prune" : "noprune"
    outfile = "velocities_$(prunestr)_$soltype.h5"
    @show outfile

    try
        @time InsarTimeseries.run_inversion(
            constant_velocity = constvel,
            stack_average = stackavg,
            max_temporal_baseline = 500,
            ignore_geo_file = "geolist_ignore.txt",
            L1 = isl1,
            prune = doprune,
            outfile = outfile,
        )
    catch
        println("Skipping $outfile")
        continue
    end

    # stackavg && continue
    # try
    #     InsarTimeseries._save_std_attrs(outfile, "velos/1")
    # catch
    #     print()
    # end
    # for ds in ["stddev_raw/1", "stddev/1"]
    #     tmp = h5read(outfile, ds)
    #     h5open(outfile, "cw") do f
    #         tmp = abs(InsarTimeseries.PHASE_TO_CM) .* read(f, ds)
    #         o_delete(f[ds])
    #         f[ds] = tmp
    #     end
    # end
end


text:
1                      # 整数，在 64 位系统中等价于 Int64
1.0                    # 64 位浮点数
true                   # 布尔型，可以是 true 或 false
'c'                    # 字符，可以是 Unicode
"abc"                  # 抽象字符串，可以有 Unicode，参见下述字符串（String）

1::Int                 # 正确，1 确实是 Int 类型
1::Float               # 错误，1 不是 Float 类型
Float64(1)             # 将 1 转化为浮点数的正确方式
Int64(1.2)             # 试图将 1.2 转化为整数，但会报错

parse(Int64, "1")      # 以 Int64 类型解读字符串 "1"
promote(true, BigInt(1)//3, 1.0)
                       # BigFloat 类型元组（参见 Tuple） , true 转换为 1.0

max_stars_repo_name: zhaiyusci/Julia-Notes

max_issues_repo_name: zhaiyusci/Julia-Notes

max_forks_repo_name: zhaiyusci/Julia-Notes


text:
"""
    collocationalchains(text::Strings; mode = "forward")

A function that takes text and returns ngram sequences of words with lexical gravity score above 5.5.
For more details, cf. (2004).

# Examples
```julia-repl
julia> collocationalchains(text)
sample output
```
"""
function collocationalchains(lowertextdata)
    intermediatearray = Array{String,1}()
    finalarray = Array{Array{String},1}()
    for ngram in ngram(tokenizestr(lowertextdata)[4:end - 3], 2)
    # i need to first clean and normalize the data before applying the lexicalgravitypair() function.
        ngram = lowercase.(ngram)
    # println(ngram)
        if lexicalgravitypair(ngram[1], ngram[2], lowertextdata) <= 5.5
            println("next")
            intermediatearray = Array{String,1}()
        else
            push!(intermediatearray, ngram[1], ngram[2])
            push!(finalarray, intermediatearray)
            println("ngram")

        end
    end
end


text:
struct DocumentFolder <: Rewriter
    dir::AbstractPath
    paths::Vector{<:AbstractPath}
    dirty::BitVector
end

function DocumentFolder(
        dir::AbstractPath;
        prefix = "",
        extensions = ("ipynb", "md"),
        includehidden = false,
        filterfn = (p) -> true)

    dir = absolute(dir)
    paths = AbstractPath[
        joinpath(relative(absolute(p), dir)) for p in walkpath(dir)
            if extension(p) in extensions && filterfn(p) && (includehidden || !ishidden(relative(absolute(p), dir)))]
    return DocumentFolder(dir, paths, trues(length(paths)))
end

function createsources!(folder::DocumentFolder)
    docs = Dict{AbstractPath, XNode}()
     for (i, p) in enumerate(folder.paths)
        if folder.dirty[i]
            docs[p] = parse(joinpath(folder.dir, p))
            folder.dirty[i] = false
        end
    end
    return docs
end

function geteventsource(folder::DocumentFolder, ch)
    watcher = LiveServer.SimpleWatcher() do filename
        @info "$filename was udpated"
        p = Path(filename)
        doc = parse(p)
        event = DocUpdated(relative(p, folder.dir), doc)
        put!(ch, event)
    end
    for p in folder.paths
        LiveServer.watch_file!(watcher, string(joinpath(folder.dir, p)))
    end
    return watcher
end

function filehandlers(folder::DocumentFolder, ::Project, ::Builder)
    return Dict(() => Dict(relative(p, folder.dir) => Pollen.parse(p)) for p in folder.paths)
end

ishidden(p::AbstractPath) = any(startswith(s, '.') && s != "." for s in p.segments)


text:
__precompile__()
module BeetleWay

using Gtk.ShortNames, GtkReactive, DataStructures#, HDF5
# const src = @__DIR__
const src = joinpath(Pkg.dir("BeetleWay"), "src")

# patches
# include(joinpath(@__DIR__, "patches.jl"))

include(joinpath(src, "log", "gui.jl"))
include(joinpath(src, "log", "preliminary_report.jl"))
include(joinpath(src, "track", "segment.jl"))
b = Builder(filename=joinpath(src, "head.glade"))
# folder = open_dialog("Select Video Folder", action=Gtk.GtkFileChooserAction.SELECT_FOLDER)
folder = joinpath(src, "..", "test", "videofolder")
# test the folder for any problems with the metadata
assert_metadata(folder)

id1 = signal_connect(_ -> log_gui(folder), b["start.log"], :clicked)
id2 = signal_connect(_ -> report_gui(folder), b["preliminary.report"], :activate)
id3 = signal_connect(_ -> fragment(folder), b["segment.videos"], :activate)
id4 = signal_connect(_ -> coordinates_gui(folder), b["track"], :clicked)

showall(b["head.window"])

end # module

# TODO:
# check the integratiy of the metadata
# maybe clearer error messages
# fix the travis errors!!!