mutable struct MeasStack{G<:Number} # G = GreensEltype

  # ETPCs (equal-time pairing correlations)
  etpc_minus::Array{Float64, 2} # "P_(y,x)", i.e. d-wave
  etpc_plus::Array{Float64, 2} # "P+(y,x)", i.e. s-wave

  # ZFPCs (zero-frequency pairing correlations)
  zfpc_minus::Array{Float64, 2} # "P_(y,x)", i.e. d-wave
  zfpc_plus::Array{Float64, 2} # "P+(y,x)", i.e. s-wave

  # ETCDCs (equal-time charge density correlations)
  etcdc_minus::Array{ComplexF64, 2} # "C_(y,x)", i.e. d-wave
  etcdc_plus::Array{ComplexF64, 2} # "C+(y,x)", i.e. s-wave

  # ZFCDCs (zero-frequency charge density correlations)
  zfcdc_minus::Array{ComplexF64, 2} # "C_(y,x)", i.e. d-wave
  zfcdc_plus::Array{ComplexF64, 2} # "C+(y,x)", i.e. s-wave

  # ZFCCCs (zero-frequency current-current correlations)
  zfccc_xx::Array{ComplexF64, 2} # "Λxx(y,x)"
  zfccc_yy::Array{ComplexF64, 2} # "Λyy(y,x)"

  # TDGF
  Gt0::Vector{Matrix{G}}
  G0t::Vector{Matrix{G}}
  BT0Inv_u_stack::Vector{Matrix{G}}
  BT0Inv_d_stack::Vector{Vector{Float64}}
  BT0Inv_t_stack::Vector{Matrix{G}}
  BBetaT_u_stack::Vector{Matrix{G}}
  BBetaT_d_stack::Vector{Vector{Float64}}
  BBetaT_t_stack::Vector{Matrix{G}}
  BT0_u_stack::Vector{Matrix{G}}
  BT0_d_stack::Vector{Vector{Float64}}
  BT0_t_stack::Vector{Matrix{G}}
  BBetaTInv_u_stack::Vector{Matrix{G}}
  BBetaTInv_d_stack::Vector{Vector{Float64}}
  BBetaTInv_t_stack::Vector{Matrix{G}}

  MeasStack{G}() where G = new{G}()
end

mutable struct Stack{G<:Number} # G = GreensEltype
  n_elements::Int
  ranges::Array{UnitRange, 1}
  current_slice::Int # running internally over 0:p.slices+1, where 0 and p.slices+1 are artifcial to prepare next sweep direction.
  direction::Int

  eye_flv::Matrix{Float64} # UPGRADE: Replace by dynamically sized I everywhere
  eye_full::Matrix{Float64} # UPGRADE: Replace by dynamically sized I everywhere
  ones_vec::Vector{Float64}

  greens::Matrix{G}
  log_det::Float64 # contains logdet of greens_{p.slices+1} === greens_1
                            # after we calculated a fresh greens in propagate()

  C::Vector{G}
  S::Vector{G}
  R::Vector{G}
  eV::SparseMatrixCSC{G,Int64}

  meas::MeasStack{G}
  Bl::Matrix{G}

  # unsure about those -> always allocate them
  curr_U::Matrix{G}
  D::Vector{Float64}
  U::Matrix{G}
  T::Matrix{G}
  d::Vector{Float64}
  tmp::Matrix{G}
  tmp2::Matrix{G}

  # global update
  gb_u_stack::Array{G, 3}
  gb_d_stack::Matrix{Float64}
  gb_t_stack::Array{G, 3}

  gb_greens::Matrix{G}
  gb_log_det::Float64

  gb_hsfield::Array{Float64, 3}

  # etgreens stack
  u_stack::Array{G, 3}
  d_stack::Matrix{Float64}
  t_stack::Array{G, 3}

  # calc detratio
  delta_i::Matrix{G}
  M::Matrix{G}
  eVop1::Matrix{G}
  eVop2::Matrix{G}
  eVop1eVop2::Matrix{G}
  Mtmp::Matrix{G}
  Mtmp2::Matrix{G}

  # calc greens
  Ul::Matrix{G}
  Ur::Matrix{G}
  Dl::Vector{Float64}
  Dr::Vector{Float64}
  Tl::Matrix{G}
  Tr::Matrix{G}

  # update greens
  A::Matrix{G}
  B::Matrix{G}
  AB::Matrix{G}

  # propagate
  greens_temp::Matrix{G}

  Stack{G}() where G = new{G}()
end

@def stackshortcuts begin
  s = mc.s
  N = mc.l.sites
  flv = mc.p.flv
  safe_mult = mc.p.safe_mult
  G = geltype(mc)
end

function allocate_global_update!(mc)
  @stackshortcuts
  # Global update backup
  s.gb_u_stack = zero(s.u_stack)
  s.gb_d_stack = zero(s.d_stack)
  s.gb_t_stack = zero(s.t_stack)
  s.gb_greens = zero(s.greens)
  s.gb_log_det = 0.
  s.gb_hsfield = zero(mc.p.hsfield)
end

function allocate_etgreens_stack!(mc)
  @stackshortcuts
  s.u_stack = zeros(G, flv*N, flv*N, s.n_elements)
  s.d_stack = zeros(Float64, flv*N, s.n_elements)
  s.t_stack = zeros(G, flv*N, flv*N, s.n_elements)
end

function allocate_calc_detratio!(mc)
  @stackshortcuts
  ## calc_detratio
  s.M = zeros(G, flv, flv)
  s.Mtmp = s.eye_flv - s.greens[1:N:end,1:N:end]
  s.delta_i = zeros(G, size(s.eye_flv))
  s.Mtmp2 = zeros(G, size(s.eye_flv))
  s.eVop1eVop2 = zeros(G, size(s.eye_flv))
  s.eVop1 = zeros(G, flv, flv)
  s.eVop2 = zeros(G, flv, flv)
end

function allocate_calc_greens!(mc)
  @stackshortcuts
  s.Ul = Matrix{G}(I, flv*N, flv*N)
  s.Ur = Matrix{G}(I, flv*N, flv*N)
  s.Tl = Matrix{G}(I, flv*N, flv*N)
  s.Tr = Matrix{G}(I, flv*N, flv*N)
  s.Dl = ones(Float64, flv*N)
  s.Dr = ones(Float64, flv*N)
end

function allocate_update_greens!(mc)
  @stackshortcuts
  ## update_greens
  s.A = s.greens[:,1:N:end]
  s.B = s.greens[1:N:end,:]
  s.AB = s.A * s.B
end

function allocate_propagate!(mc)
  @stackshortcuts
  s.greens_temp = zeros(G, flv*N, flv*N)
end

function _initialize_stack(mc::AbstractDQMC)
  @stackshortcuts
  s.n_elements = convert(Int, mc.p.slices / safe_mult) + 1

  s.ranges = UnitRange[]
  for i in 1:s.n_elements - 1
    push!(s.ranges, 1 + (i - 1) * safe_mult:i * safe_mult)
  end

  s.eye_flv = Matrix{Float64}(I, flv, flv)
  s.eye_full = Matrix{Float64}(I, flv*N,flv*N)
  s.ones_vec = ones(flv*N)

  s.greens = zeros(G, flv*N, flv*N)

  # interaction matrix
  s.C = zeros(G, N)
  s.S = zeros(G, N)
  s.R = zeros(G, N)
  s.eV = spzeros(G, flv*N, flv*N)

  # init empty meas stack
  s.meas = MeasStack{G}()

  # slice matrix
  cbtype(mc) === CBFalse && (s.Bl = zeros(G, flv*N, flv*N))

  # unsure about those
  s.curr_U = zeros(G, flv*N, flv*N) # used in tdgf, add_slice_sequence_ and propagate
  s.D = zeros(Float64, flv*N) # used in calc_greens in mc (below) and in calc greens in fermion meas.
  s.d = zeros(Float64, flv*N) # same as above
  s.tmp = zeros(G, flv*N, flv*N)
  s.tmp2 = zeros(G, flv*N, flv*N)
  s.U = zeros(G, flv*N, flv*N)
  s.T = zeros(G, flv*N, flv*N)
end

function initialize_stack(mc::AbstractDQMC)
  @mytimeit mc.a.to "initialize_stack" begin
    _initialize_stack(mc)

    # allocate for dqmc
    allocate_etgreens_stack!(mc)
    allocate_global_update!(mc)
    allocate_calc_detratio!(mc)
    allocate_calc_greens!(mc)
    allocate_update_greens!(mc)
    allocate_propagate!(mc)

    # allocate for measurements during dqmc
    # allocate_etpc!(mc)

  end #timeit

  nothing
end

function build_stack(mc::AbstractDQMC)
  p = mc.p
  s = mc.s
  a = mc.a

  @mytimeit a.to "build_stack" begin

  s.u_stack[:, :, 1] = s.eye_full
  s.d_stack[:, 1] = s.ones_vec
  s.t_stack[:, :, 1] = s.eye_full

  @inbounds for i in 1:length(s.ranges)
    add_slice_sequence_left(mc, i)
  end

  s.current_slice = p.slices + 1
  s.direction = -1

  end #timeit

  nothing
end

"""
Updates stack[idx+1] based on stack[idx]
"""
function add_slice_sequence_left(mc::AbstractDQMC, idx::Int)
  s = mc.s
  a = mc.a

  copyto!(s.curr_U, s.u_stack[:, :, idx])

  # println("Adding slice seq left $idx = ", s.ranges[idx])
  for slice in s.ranges[idx]
    multiply_B_left!(mc, slice, s.curr_U)
  end

  @views rmul!(s.curr_U, Diagonal(s.d_stack[:, idx]))
  @mytimeit a.to "decompose_udt!" s.u_stack[:, :, idx + 1], T = @views decompose_udt!(s.curr_U, s.d_stack[:, idx + 1])
  @views mul!(s.t_stack[:, :, idx + 1],  T, s.t_stack[:, :, idx])
  nothing
end

"""
Updates stack[idx] based on stack[idx+1]
"""
function add_slice_sequence_right(mc::AbstractDQMC, idx::Int)
  s = mc.s
  a = mc.a

  copyto!(s.curr_U, s.u_stack[:, :, idx + 1])

  for slice in reverse(s.ranges[idx])
    multiply_daggered_B_left!(mc, slice, s.curr_U)
  end

  @views rmul!(s.curr_U, Diagonal(s.d_stack[:, idx + 1]))
  @mytimeit a.to "decompose_udt!" s.u_stack[:, :, idx], T = @views decompose_udt!(s.curr_U, s.d_stack[:, idx])
  @views mul!(s.t_stack[:, :, idx], T, s.t_stack[:, :, idx + 1])
  nothing
end

function wrap_greens!(mc::AbstractDQMC, gf::Matrix, curr_slice::Int=mc.s.current_slice, direction::Int=mc.s.direction)
  if direction == -1
    multiply_B_inv_left!(mc, curr_slice - 1, gf)
    multiply_B_right!(mc, curr_slice - 1, gf)
  else
    multiply_B_left!(mc, curr_slice, gf)
    multiply_B_inv_right!(mc, curr_slice, gf)
  end
  nothing
end

function wrap_greens(mc::AbstractDQMC, gf::Matrix,slice::Int=mc.s.current_slice,direction::Int=mc.s.direction)
  temp = copy(gf)
  wrap_greens!(mc, temp, slice, direction)
  return temp
end

"""
Calculates G(slice) using s.Ur,s.Dr,s.Tr=B(slice)' ... B(M)' and s.Ul,s.Dl,s.Tl=B(slice-1) ... B(1)
"""
function calculate_greens(mc::AbstractDQMC)
  # TODO: Use inv_one_plus_two_udts from linalg.jl here
  s = mc.s
  tmp = mc.s.tmp
  tmp2 = mc.s.tmp2
  a = mc.a
  @mytimeit a.to "calculate_greens" begin

  mul!(tmp, s.Tl, adjoint(s.Tr))
  rmul!(tmp, Diagonal(s.Dr))
  lmul!(Diagonal(s.Dl), tmp)
  @mytimeit a.to "decompose_udt!" s.U, s.T = decompose_udt!(tmp, s.D)

  mul!(tmp, s.Ul, s.U)
  s.U .= tmp
  mul!(tmp2, s.T, adjoint(s.Ur))
  myrdiv!(tmp, s.U', tmp2) # or tmp = s.U' / tmp2 or mul!(tmp, adjoint(s.U), inv(tmp2))
  tmp[diagind(tmp)] .+= s.D
  @mytimeit a.to "decompose_udt!" u, t = decompose_udt!(tmp, s.d)

  mul!(tmp, t, tmp2)
  s.T = inv(tmp)
  mul!(tmp, s.U, u)
  s.U = adjoint(tmp)
  s.d .= 1 ./ s.d

  copyto!(tmp2, s.U)
  lmul!(Diagonal(s.d), tmp2)
  mul!(s.greens, s.T, tmp2)
  end #timeit
  nothing
end

"""
Only reasonable immediately after calculate_greens() because it depends on s.U, s.d and s.T!
"""
function calculate_logdet(mc::AbstractDQMC)
  s = mc.s

  if mc.p.opdim == 1
    s.log_det = real(log(complex(det(s.U))) + sum(log.(s.d)) + log(complex(det(s.T))))
  else
    s.log_det = real(logdet(s.U) + sum(log.(s.d)) + logdet(s.T))
  end
end

################################################################################
# Propagation
################################################################################
function propagate(mc::AbstractDQMC)
  s = mc.s
  p = mc.p

  if s.direction == 1
    if mod(s.current_slice, p.safe_mult) == 0
      s.current_slice +=1 # slice we are going to
      # println("we are going to $(s.current_slice) with a fresh gf.")
      if s.current_slice == 1
        s.Ur[:, :], s.Dr[:], s.Tr[:, :] = s.u_stack[:, :, 1], s.d_stack[:, 1], s.t_stack[:, :, 1]
        s.u_stack[:, :, 1] = s.eye_full
        s.d_stack[:, 1] = s.ones_vec
        s.t_stack[:, :, 1] = s.eye_full
        s.Ul[:,:], s.Dl[:], s.Tl[:,:] = s.u_stack[:, :, 1], s.d_stack[:, 1], s.t_stack[:, :, 1]

        calculate_greens(mc) # greens_1 ( === greens_{m+1} )
        calculate_logdet(mc)

      elseif 1 < s.current_slice <= p.slices
        idx = Int((s.current_slice - 1)/p.safe_mult)

        s.Ur[:, :], s.Dr[:], s.Tr[:, :] = s.u_stack[:, :, idx+1], s.d_stack[:, idx+1], s.t_stack[:, :, idx+1]
        add_slice_sequence_left(mc, idx)
        s.Ul[:,:], s.Dl[:], s.Tl[:,:] = s.u_stack[:, :, idx+1], s.d_stack[:, idx+1], s.t_stack[:, :, idx+1]

        if p.all_checks
          copyto!(s.greens_temp, s.greens)
        end

        wrap_greens!(mc, s.greens_temp, s.current_slice - 1, 1)

        calculate_greens(mc) # greens_{slice we are propagating to}

        if p.all_checks
          diff = maximum(absdiff(s.greens_temp, s.greens))
          if diff > 1e-7
            @printf("->%d \t+1 Propagation instability\t %.4f\n", s.current_slice, diff)
          end
        end

      else # we are going to p.slices+1
        idx = s.n_elements - 1
        add_slice_sequence_left(mc, idx)
        s.direction = -1
        s.current_slice = p.slices+1 # redundant
        propagate(mc)
      end

    else
      # Wrapping
      wrap_greens!(mc, s.greens, s.current_slice, 1)

      s.current_slice += 1
    end

  else # s.direction == -1
    if mod(s.current_slice-1, p.safe_mult) == 0
      s.current_slice -= 1 # slice we are going to
      # println("we are going to $(s.current_slice) with a fresh+wrapped gf.")
      if s.current_slice == p.slices
        s.Ul[:, :], s.Dl[:], s.Tl[:, :] = s.u_stack[:, :, end], s.d_stack[:, end], s.t_stack[:, :, end]
        s.u_stack[:, :, end] = s.eye_full
        s.d_stack[:, end] = s.ones_vec
        s.t_stack[:, :, end] = s.eye_full
        s.Ur[:,:], s.Dr[:], s.Tr[:,:] = s.u_stack[:, :, end], s.d_stack[:, end], s.t_stack[:, :, end]

        calculate_greens(mc) # greens_{p.slices+1} === greens_1
        calculate_logdet(mc) # calculate logdet for potential global update

        # wrap to greens_{p.slices}
        wrap_greens!(mc, s.greens, s.current_slice + 1, -1)

      elseif 0 < s.current_slice < p.slices
        idx = Int(s.current_slice / p.safe_mult) + 1
        s.Ul[:, :], s.Dl[:], s.Tl[:, :] = s.u_stack[:, :, idx], s.d_stack[:, idx], s.t_stack[:, :, idx]
        add_slice_sequence_right(mc, idx)
        s.Ur[:,:], s.Dr[:], s.Tr[:,:] = s.u_stack[:, :, idx], s.d_stack[:, idx], s.t_stack[:, :, idx]

        if p.all_checks
          copyto!(s.greens_temp, s.greens)
        end

        calculate_greens(mc)

        if p.all_checks
          diff = maximum(absdiff(s.greens_temp, s.greens))
          if diff > 1e-7
            @printf("->%d \t-1 Propagation instability\t %.4f\n", s.current_slice, diff)
          end
        end

        wrap_greens!(mc, s.greens, s.current_slice + 1, -1)

      else # we are going to 0
        idx = 1
        add_slice_sequence_right(mc, idx)
        s.direction = 1
        s.current_slice = 0 # redundant
        propagate(mc)
      end

    else
      # Wrapping
      wrap_greens!(mc, s.greens, s.current_slice, -1)
      s.current_slice -= 1
    end
  end
  nothing
end


using Base58, BitConverter
@testset "Script" begin
    @testset "parse" begin
        script_pubkey = IOBuffer(hex2bytes("6a47304402207899531a52d59a6de200179928ca900254a36b8dff8bb75f5f5d71b1cdc26125022008b422690b8461cb52c3cc30330b23d574351872b7c361e9aae3649071c1a7160121035d5c93d9ac96881f19ba1f686f15f009ded7c62efe85a872e6a19b43c15a2937"))
        script = Bitcoin.scriptparse(script_pubkey)
        want = hex2bytes("304402207899531a52d59a6de200179928ca900254a36b8dff8bb75f5f5d71b1cdc26125022008b422690b8461cb52c3cc30330b23d574351872b7c361e9aae3649071c1a71601")
        @test script.instructions[1] == want
        want = hex2bytes("035d5c93d9ac96881f19ba1f686f15f009ded7c62efe85a872e6a19b43c15a2937")
        @test script.instructions[2] == want
        script_pubkey = IOBuffer(hex2bytes("fdfe0000483045022100c679944ff8f20373685e1122b581f64752c1d22c67f6f3ae26333aa9c3f43d730220793233401f87f640f9c39207349ffef42d0e27046755263c0a69c436ab07febc01483045022100eadc1c6e72f241c3e076a7109b8053db53987f3fcc99e3f88fc4e52dbfd5f3a202201f02cbff194c41e6f8da762e024a7ab85c1b1616b74720f13283043e9e99dab8014c69522102b0c7be446b92624112f3c7d4ffc214921c74c1cb891bf945c49fbe5981ee026b21039021c9391e328e0cb3b61ba05dcc5e122ab234e55d1502e59b10d8f588aea4632102f3bd8f64363066f35968bd82ed9c6e8afecbd6136311bb51e91204f614144e9b53aeffffffff05a08601000000000017a914081fbb6ec9d83104367eb1a6a59e2a92417d79298700350c00000000001976a914677345c7376dfda2c52ad9b6a153b643b6409a3788acc7f341160000000017a914234c15756b9599314c9299340eaabab7f1810d8287c02709000000000017a91469be3ca6195efcab5194e1530164ec47637d44308740420f00000000001976a91487fadba66b9e48c0c8082f33107fdb01970eb80388ac00000000"))
        want = hex2bytes("522102b0c7be446b92624112f3c7d4ffc214921c74c1cb891bf945c49fbe5981ee026b21039021c9391e328e0cb3b61ba05dcc5e122ab234e55d1502e59b10d8f588aea4632102f3bd8f64363066f35968bd82ed9c6e8afecbd6136311bb51e91204f614144e9b53ae")
        script = Bitcoin.scriptparse(script_pubkey)
        @test script.instructions[4] == want
    end
    @testset "Serialize" begin
        want = "6a47304402207899531a52d59a6de200179928ca900254a36b8dff8bb75f5f5d71b1cdc26125022008b422690b8461cb52c3cc30330b23d574351872b7c361e9aae3649071c1a7160121035d5c93d9ac96881f19ba1f686f15f009ded7c62efe85a872e6a19b43c15a2937"
        script_pubkey = IOBuffer(hex2bytes(want))
        script = Bitcoin.scriptparse(script_pubkey)
        @test bytes2hex(Bitcoin.serialize(script)) == want
    end
    @testset "Evaluate" begin
        modified_tx = hex2bytes("0100000001813f79011acb80925dfe69b3def355fe914bd1d96a3f5f71bf8303c6a989c7d1000000001976a914a802fc56c704ce87c42d7c92eb75e7896bdc41ae88acfeffffff02a135ef01000000001976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac99c39800000000001976a9141c4bc762dd5423e332166702cb75f40df79fea1288ac1943060001000000")
        h256 = Bitcoin.hash256(modified_tx)
        z = to_int(h256)
        stream = IOBuffer(hex2bytes("0100000001813f79011acb80925dfe69b3def355fe914bd1d96a3f5f71bf8303c6a989c7d1000000006b483045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031ccfcf21320b0277457c98f02207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9c8e10615bed01210349fc4e631e3624a545de3f89f5d8684c7b8138bd94bdd531d2e213bf016b278afeffffff02a135ef01000000001976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac99c39800000000001976a9141c4bc762dd5423e332166702cb75f40df79fea1288ac19430600"))
        tx = parse(stream)::Tx
        tx_in = tx.tx_ins[1]
        combined_script = Bitcoin.Script(nothing)
        append!(combined_script.instructions, copy(tx_in.script_sig.instructions))
        append!(combined_script.instructions, copy(Bitcoin.script_pubkey(tx_in).instructions))
        @test Bitcoin.evaluate(combined_script, z) == true
    end
    @testset "Address" begin
        address_1 = "1BenRpVUFK65JFWcQSuHnJKzc4M8ZP8Eqa"
        h160 = base58checkdecode(UInt8.(collect(address_1)))[2:end]
        p2pkh_script_pubkey = Bitcoin.p2pkh_script(h160)
        @test script2address(p2pkh_script_pubkey, false) == address_1
        address_2 = "mrAjisaT4LXL5MzE81sfcDYKU3wqWSvf9q"
        @test script2address(p2pkh_script_pubkey, true) == address_2
        address_3 = "3CLoMMyuoDQTPRD3XYZtCvgvkadrAdvdXh"
        h160 = base58checkdecode(UInt8.(collect(address_3)))[2:end]
        p2sh_script_pubkey = Bitcoin.p2sh_script(h160)
        @test script2address(p2sh_script_pubkey, false) == address_3
        address_4 = "2N3u1R6uwQfuobCqbCgBkpsgBxvr1tZpe7B"
        @test script2address(p2sh_script_pubkey, true) == address_4
    end
    @testset "P2WPKH" begin
        h160 = hex2bytes("74d691da1574e6b3c192ecfb52cc8984ee7b6c56")
        p2wpkh_script = Bitcoin.p2wpkh_script(h160)
        @test Bitcoin.is_p2wpkh(p2wpkh_script)
    end
end


"""
Type for storing data acquired with a transient response (TR) experimental paradigm.

#### Example

Put an example here

```julia
s = read_TR("filename")
s = rereference(s, "Cz")
```

"""
mutable struct TR <: EEG
    data::Array
    sensors::Array{Sensor}
    triggers::Dict
    system_codes::Dict
    samplingrate::typeof(1.0u"Hz")
    reference_channel::Array{AbstractString,1}
    file_path::AbstractString
    file_name::AbstractString
    processing::Dict
    header::Dict
end

#######################################
#
# Show
#
#######################################

import Base.show
function Base.show(io::IO, a::TR)
    time_length = round.(size(a.data, 1) / samplingrate(Float64, a) / 60)
    println(
        io,
        "Transient response measurement of $time_length mins with $(size(a.data,2)) channels sampled at $(samplingrate(a))",
    )

end

#######################################
#
# Read data
#
#######################################

"""
    read_TR(fname::AbstractString)
    read_TR(args...)

Read a file or IO stream and store the data in an `TR` type.

# Arguments

- `fname`: Name of the file to be read
- `min_epoch_length`: Minimum epoch length in samples. Shorter epochs will be removed (0)
- `max_epoch_length`: Maximum epoch length in samples. Longer epochs will be removed (0 = all)
- `valid_triggers`: Triggers that are considered valid, others are removed ([1,2])
- `stimulation_amplitude`: Amplitude of stimulation (NaN)
- `remove_first`: Number of epochs to be removed from start of recording (0)
- `max_epochs`: Maximum number of epochs to retain (0 = all)

# Supported file formats

- BIOSEMI (.bdf)
"""
function read_TR(
    fname::AbstractString;
    valid_triggers::Array{Int} = [1, 2],
    min_epoch_length::Int = 0,
    max_epoch_length::Int = 0,
    remove_first::Int = 0,
    max_epochs::Int = 0,
    kwargs...,
)

    @info("Importing EEG from file: $fname")
    file_path, file_name, ext = fileparts(fname)

    #
    # Read file data
    #

    # Import raw data
    if ext == "bdf"
        data, triggers, system_codes, samplingrate, reference_channel, header =
            import_biosemi(fname; kwargs...)
    else
        warn("File type $ext is unknown")
    end

    # Create electrodes
    elecs = Electrode[]
    for e in header["chanLabels"]
        push!(elecs, Electrode(e, Talairach(NaN * u"m", NaN * u"m", NaN * u"m"), Dict()))
    end

    # Create EEG type
    if unit(samplingrate) == unit(1.0u"Hz")
        #nothing
    else
        samplingrate = samplingrate * 1.0u"Hz"
    end

    a = TR(
        data,
        elecs,
        triggers,
        system_codes,
        samplingrate,
        [reference_channel],
        file_path,
        file_name,
        Dict(),
        header,
    )

    #
    # Clean up
    #

    # Remove status channel information
    remove_channel!(a, "Status")

    # Clean epoch index
    a.triggers = clean_triggers(
        a.triggers,
        valid_triggers,
        min_epoch_length,
        max_epoch_length,
        remove_first,
        max_epochs,
    )

    # Try and match sensor names to known locations
    locs = read_elp(joinpath(datadep"BioSemi64Locations", "biosemi64.elp"))
    new_sens, idx = match_sensors(locs, labels(sensors(a)))
    if length(new_sens) == length(sensors(a))
        @debug("Sucsessfully matches location of all sensors, using new locations")
        a.sensors = new_sens
    end

    return a
end


# MIT license
# Copyright (c) Microsoft Corporation. All rights reserved.
# See LICENSE in the project root for full license information.

export cooketriplet, doubleconvexlensonly

#Hardcode these glass types so this code will work even if it is not possible to download the glasses at build time.
const Examples_N_BK7 = GlassCat.Glass("$(@__MODULE__).Examples_N_BK7", 2, 1.03961212, 0.00600069867, 0.231792344, 0.0200179144, 1.01046945, 103.560653, 0.0, 0.0, NaN, NaN, 0.3, 2.5, 1.86e-6, 1.31e-8, -1.37e-11, 4.34e-7, 6.27e-10, 0.17, 20.0, -0.0009, 2.3, 1.0, 7.1, 1.0, 1, 1.0, [(0.3, 0.05, 25.0), (0.31, 0.25, 25.0), (0.32, 0.52, 25.0), (0.334, 0.78, 25.0), (0.35, 0.92, 25.0), (0.365, 0.971, 25.0), (0.37, 0.977, 25.0), (0.38, 0.983, 25.0), (0.39, 0.989, 25.0), (0.4, 0.992, 25.0), (0.405, 0.993, 25.0), (0.42, 0.993, 25.0), (0.436, 0.992, 25.0), (0.46, 0.993, 25.0), (0.5, 0.994, 25.0), (0.546, 0.996, 25.0), (0.58, 0.995, 25.0), (0.62, 0.994, 25.0), (0.66, 0.994, 25.0), (0.7, 0.996, 25.0), (1.06, 0.997, 25.0), (1.53, 0.98, 25.0), (1.97, 0.84, 25.0), (2.325, 0.56, 25.0), (2.5, 0.36, 25.0)], 1.5168, 2.3, 0.0, 0, 64.17, 0, 2.51, 0.0)

const Examples_N_SK16 =GlassCat.Glass("$(@__MODULE__).Examples_N_SK16", 2, 1.34317774, 0.00704687339, 0.241144399, 0.0229005, 0.994317969, 92.7508526, 0.0, 0.0, NaN, NaN, 0.31, 2.5, -2.37e-8, 1.32e-8, -1.29e-11, 4.09e-7, 5.17e-10, 0.17, 20.0, -0.0011, 3.2, 1.4, 6.3, 4.0, 1, 53.3, [(0.31, 0.02, 25.0), (0.32, 0.11, 25.0), (0.334, 0.4, 25.0), (0.35, 0.7, 25.0), (0.365, 0.86, 25.0), (0.37, 0.89, 25.0), (0.38, 0.93, 25.0), (0.39, 0.956, 25.0), (0.4, 0.97, 25.0), (0.405, 0.974, 25.0), (0.42, 0.979, 25.0), (0.436, 0.981, 25.0), (0.46, 0.984, 25.0), (0.5, 0.991, 25.0), (0.546, 0.994, 25.0), (0.58, 0.994, 25.0), (0.62, 0.993, 25.0), (0.66, 0.994, 25.0), (0.7, 0.996, 25.0), (1.06, 0.995, 25.0), (1.53, 0.973, 25.0), (1.97, 0.88, 25.0), (2.325, 0.54, 25.0), (2.5, 0.26, 25.0)], 1.62041, 3.3, 4.0, 0, 60.32, 0, 3.58, 0.0)

const Examples_N_SF2 = GlassCat.Glass("$(@__MODULE__).Examples_N_SF2", 2, 1.47343127, 0.0109019098, 0.163681849, 0.0585683687, 1.36920899, 127.404933, 0.0, 0.0, NaN, NaN, 0.365, 2.5, 3.1e-6, 1.75e-8, 6.62e-11, 7.51e-7, 8.99e-10, 0.277, 20.0, 0.0081, 1.0, 1.4, 6.68, 1.0, 1, 1.0, [(0.365, 0.007, 25.0), (0.37, 0.06, 25.0), (0.38, 0.4, 25.0), (0.39, 0.68, 25.0), (0.4, 0.83, 25.0), (0.405, 0.865, 25.0), (0.42, 0.926, 25.0), (0.436, 0.949, 25.0), (0.46, 0.961, 25.0), (0.5, 0.975, 25.0), (0.546, 0.986, 25.0), (0.58, 0.987, 25.0), (0.62, 0.984, 25.0), (0.66, 0.984, 25.0), (0.7, 0.987, 25.0), (1.06, 0.997, 25.0), (1.53, 0.984, 25.0), (1.97, 0.93, 25.0), (2.325, 0.76, 25.0), (2.5, 0.67, 25.0)], 1.64769, 1.2, 0.0, 0, 33.82, 0, 2.718, 0.0)

const Examples_N_SF14 = GlassCat.Glass("$(@__MODULE__).Examples_N_SF14", 2, 1.69022361, 0.0130512113, 0.288870052, 0.061369188, 1.7045187, 149.517689, 0.0, 0.0, NaN, NaN, 0.365, 2.5, -5.56e-6, 7.09e-9, -1.09e-11, 9.85e-7, 1.39e-9, 0.287, 20.0, 0.013, 1.0, 2.2, 9.41, 1.0, 1, 1.0, [(0.365, 0.004, 25.0), (0.37, 0.04, 25.0), (0.38, 0.33, 25.0), (0.39, 0.61, 25.0), (0.4, 0.75, 25.0), (0.405, 0.79, 25.0), (0.42, 0.87, 25.0), (0.436, 0.91, 25.0), (0.46, 0.938, 25.0), (0.5, 0.964, 25.0), (0.546, 0.983, 25.0), (0.58, 0.987, 25.0), (0.62, 0.987, 25.0), (0.66, 0.987, 25.0), (0.7, 0.985, 25.0), (1.06, 0.998, 25.0), (1.53, 0.98, 25.0), (1.97, 0.88, 25.0), (2.325, 0.64, 25.0), (2.5, 0.57, 25.0)], 1.76182, 1.0, 0.0, 0, 26.53, 0, 3.118, 0.0)

"""
    hemisphere()

Create a geometric hemisphere
"""
function hemisphere()::CSGTree
    sph = Sphere(10.0)
    pln = Plane(0.0, 0.0, -1.0, 0.0, 0.0, 0.0)
    # CSV operations create a csggenerator which instantiates the csg tree after applying a rigid body transformation.
    # This allows you to make as many instances of the object as you want with different transformations. We just want
    # the CSGTree object rather than a generator.
    return (sph ∩ pln)()
end

"""
    opticalhemisphere()

Create an optical hemisphere that has optical material properties so it will reflect and refract light. In the previous
example the hemisphere object had optical properties of Air, which is the default optical interface, so it won't refract
or reflect light.
"""
function opticalhemisphere()::CSGOpticalSystem
    sph = Sphere(10.0, interface = FresnelInterface{Float64}(Examples_N_BK7, Air))
    pln = Plane(0.0, 0.0, -1.0, 0.0, 0.0, 0.0, interface = FresnelInterface{Float64}(Examples_N_BK7, Air))
    assy = LensAssembly{Float64}((sph ∩ pln)())
    return CSGOpticalSystem(assy, Rectangle(1.0, 1.0, SVector{3,Float64}(0.0, 0.0, 1.0), SVector{3,Float64}(0.0, 0.0, -11.0)))
end

function cooketriplet(::Type{T} = Float64, detpix::Int = 1000) where {T<:Real}
    AxisymmetricOpticalSystem{T}(
        DataFrame(
            SurfaceType = ["Object", "Standard", "Standard", "Standard", "Stop", "Standard", "Standard", "Image"],
            Radius = [Inf, 26.777, 66.604, -35.571, 35.571, 35.571, -26.777, Inf],
            # OptimizeRadius = [false, true, true, true, true, true, true, false],
            Thickness = [Inf, 4.0, 2.0, 4.0, 2.0, 4.0, 44.748, missing],
            # OptimizeThickness = [false, true, true, true, true, true, true, false],
            Material = [Air, Examples_N_SK16, Air, Examples_N_SF2, Air, Examples_N_SK16, Air, missing],
            SemiDiameter = [Inf, 8.580, 7.513, 7.054, 6.033, 7.003, 7.506, 15.0]
        ),
        detpix,
        detpix
    )
end

function cooketripletfirstelement(::Type{T} = Float64) where {T<:Real}
    AxisymmetricOpticalSystem(
        DataFrame(
            SurfaceType = ["Object", "Standard", "Standard", "Image"],
            Radius = [Inf, -35.571, 35.571, Inf],
            Thickness = [Inf, 4.0, 44.748, missing],
            Material = [Air, Examples_N_SK16, Air, missing],
            SemiDiameter = [Inf, 7.054, 6.033, 15.0]
        )
    )
end

function convexplano(::Type{T} = Float64) where {T<:Real}
    AxisymmetricOpticalSystem{T}(
        DataFrame(
            SurfaceType = ["Object", "Standard", "Standard", "Image"],
            Radius = [Inf, 60.0, Inf, Inf],
            Thickness = [Inf, 10.0, 57.8, missing],
            Material = [Air, Examples_N_BK7, Air, missing],
            SemiDiameter = [Inf, 9.0, 9.0, 15.0]
        )
    )
end

function doubleconvex(frontradius::T, rearradius::T) where {T<:Real}
    AxisymmetricOpticalSystem{T}(
        DataFrame(
            SurfaceType = ["Object", "Standard", "Standard", "Image"],
            Radius = [convert(T, Inf64), frontradius, rearradius, convert(T, Inf64)],
            # OptimizeRadius = [false, true, true, false],
            Thickness = [convert(T, Inf64), convert(T, 10.0), convert(T, 57.8), missing],
            # OptimizeThickness = [false, false, false, false],

            Material = [Air, Examples_N_BK7, Air, missing],
            SemiDiameter = [convert(T, Inf64), convert(T, 9.0), convert(T, 9.0), convert(T, 15.0)]
        )
    )
end

function doubleconvexconic(::Type{T} = Float64) where {T<:Real}
    AxisymmetricOpticalSystem{T}(
        DataFrame(
            SurfaceType = ["Object", "Standard", "Standard", "Image"],
            Radius = [Inf64, 60, -60, Inf64],
            # OptimizeRadius = [false, true, true, false],
            Thickness = [Inf64, 10.0, 57.8, missing],
            # OptimizeThickness = [false, false, false, false],
            Conic = [missing, 0.01, 0.01, missing],
            # OptimizeConic = [false, true, true, false],
            Material = [Air, Examples_N_BK7, Air, missing],
            SemiDiameter = [Inf64, 9.0, 9.0, 15.0]
        )
    )
end

function doubleconvexlensonly(frontradius::T,rearradius::T) where{T<:Real}
    AxisymmetricLens{T}(
        DataFrame(
            SurfaceType = ["Object", "Standard", "Standard", "Image"],
            Radius = [convert(T, Inf64), frontradius, rearradius, convert(T, Inf64)],
            # OptimizeRadius = [false, true, true, false],
            Thickness = [convert(T, Inf64), convert(T, 10.0), convert(T, 57.8), missing],
            # OptimizeThickness = [false, false, false, false],
            Material = [Air, Examples_N_BK7, Air, missing],
            SemiDiameter = [convert(T, Inf64), convert(T, 9.0), convert(T, 9.0), convert(T, 15.0)]
        )
    )
end

function doubleconvex(
    ::Type{T} = Float64;
    temperature::Unitful.Temperature = GlassCat.TEMP_REF_UNITFUL,
    pressure::T = convert(T, PRESSURE_REF)
) where {T<:Real}
    AxisymmetricOpticalSystem{T}(
        DataFrame(
            SurfaceType = ["Object", "Standard", "Standard", "Image"],
            Radius = [Inf64, 60, -60, Inf64],
            # OptimizeRadius = [false, true, true, false],
            Thickness = [Inf64, 10.0, 57.8, missing],
            # OptimizeThickness = [false, true, true, false],
            Material = [Air, Examples_N_BK7, Air, missing],
            SemiDiameter = [Inf64, 9.0, 9.0, 15.0]
        );
        temperature,
        pressure
    )
end

function doubleconcave(::Type{T} = Float64) where {T<:Real}
    AxisymmetricOpticalSystem{T}(
        DataFrame(
            SurfaceType = ["Object", "Standard", "Standard", "Image"],
            Radius = [Inf64, -41.0, 41.0, Inf64],
            Thickness = [Inf64, 10.0, 57.8, missing],
            Material = [Air, Examples_N_BK7, Air, missing],
            SemiDiameter = [Inf64, 9.0, 9.0, 15.0]
        )
    )
end

function planoconcaverefl(::Type{T} = Float64) where {T<:Real}
    AxisymmetricOpticalSystem{T}(
        DataFrame(
            SurfaceType = ["Object", "Standard", "Standard", "Image"],
            Radius = [Inf64, Inf64, -41.0, Inf64],
            Thickness = [Inf64, 10.0, -57.8, missing],
            Material = [Air, Examples_N_BK7, Air, missing],
            SemiDiameter = [Inf64, 9.0, 9.0, 25.0],
            Reflectance = [missing, missing, 1.0, missing]
        )
    )
end

function concaveplano(::Type{T} = Float64) where {T<:Real}
    AxisymmetricOpticalSystem{T}(
        DataFrame(
            SurfaceType = ["Object", "Standard", "Standard", "Image"],
            Radius = [Inf64, -41.0, Inf64, Inf64],
            Thickness = [Inf64, 10.0, 57.8, missing],
            Material = [Air, Examples_N_BK7, Air, missing],
            SemiDiameter = [Inf64, 9.0, 9.0, 15.0]
        )
    )
end

function planoplano(::Type{T} = Float64) where {T<:Real}
    AxisymmetricOpticalSystem{T}(
        DataFrame(
            SurfaceType = ["Object", 1, 2, "Image"],
            Radius = [Inf64, Inf64, Inf64, Inf64],
            Thickness = [Inf64, 10.0, 57.8, missing],
            Material = [Air, Examples_N_BK7, Air, missing],
            SemiDiameter = [Inf64, 9.0, 9.0, 15.0]
        )
    )
end

function autodrawrays(lens::AxisymmetricOpticalSystem = cooketriplet(), angle = 10; kwargs...)
    f1 = HexapolarField(lens, collimated = true, wavelength = 0.45, sourcenum = 1)
    Vis.drawtracerays(lens, raygenerator = f1, test = true, trackallrays = true, colorbysourcenum = true; kwargs...)
    f2 = HexapolarField(lens, collimated = true, wavelength = 0.45, sourceangle = angle / 180 * π, sourcenum = 2)
    Vis.drawtracerays!(lens, raygenerator = f2, test = true, trackallrays = true, colorbysourcenum = true; kwargs...)
end

function autospotdiag(lens::AxisymmetricOpticalSystem = cooketriplet(); kwargs...)
    f1 = HexapolarField(lens, collimated = true, wavelength = 0.45, sourcenum = 1)
    f2 = HexapolarField(lens, collimated = true, wavelength = 0.45, sourceangle = 5 / 180 * π, sourcenum = 2)
    f3 = HexapolarField(lens, collimated = true, wavelength = 0.45, sourceangle = 10 / 180 * π, sourcenum = 3)
    Vis.spotdiaggrid(lens, [f1, f2, f3]; kwargs...)
end

# Display the spot diagram of a simple cooketriplet lens
function hexapolarspotdiagramexample(lens = cooketriplet(), numrings::Int = 5, angle = 0.0)
    Vis.spotdiag(lens, samples = numrings, sourceangle = angle)
end

function cartesiangridspotdiagramexample(lens = cooketriplet(), numsamples::Int = 5, angle = 0.0)
    Vis.spotdiag(lens, hexapolar = false, samples = numsamples, sourceangle = angle)
end

function prism_refraction()
    # build the triangular prism
    int = FresnelInterface{Float64}(Examples_N_SF14, Air)
    s = 2.0
    prism = (
        Plane(
            SVector(0.0, -1.0, 0.0),
            SVector(0.0, -s, 0.0),
            interface = int,
            vishalfsizeu = 2 * s,
            vishalfsizev = 2 * s
        ) ∩
        Plane(
            SVector(0.0, sind(30), cosd(30)),
            SVector(0.0, s * sind(30), s * cosd(30)),
            interface = int,
            vishalfsizeu = 2 * s,
            vishalfsizev = 2 * s
        ) ∩
        Plane(
            SVector(0.0, sind(30), -cosd(30)),
            SVector(0.0, s * sind(30), -s * cosd(30)),
            interface = int,
            vishalfsizeu = 2 * s,
            vishalfsizev = 2 * s
        )
    )
    sys = CSGOpticalSystem(LensAssembly(prism()), Rectangle(15.0, 15.0, SVector(0.0, 0.0, 1.0), SVector(0.0, 0.0, -20.0), interface = opaqueinterface()))
    # create some 'white' light
    rays = Vector{OpticalRay{Float64,3}}(undef, 0)
    for i in 0:7
        λ = ((i / 7) * 200 + 450) / 1000
        r = OpticalRay(SVector(0.0, -3.0, 10.0), SVector(0.0, 0.5, -1.0), 1.0, λ)
        push!(rays, r)
    end
    raygen = RayListSource(rays)
    # draw the result
    Vis.drawtracerays(sys, raygenerator = raygen, test = true, trackallrays = true)
end

function fresnel(convex = true; kwargs...)
    lens = FresnelLens(Examples_N_BK7, 0.0, convex ? 15.0 : -15.0, 1.0, 8.0, 0.8, conic = 0.1)
    sys = CSGOpticalSystem(LensAssembly(lens()), Rectangle(15.0, 15.0, SVector(0.0, 0.0, 1.0), SVector(0.0, 0.0, -25.0), interface = opaqueinterface()))
    Vis.drawtracerays(sys; test = true, trackallrays = true, numdivisions = 30, kwargs...)
end

function eyetrackHOE(nrays = 5000, det = false, showhead = true, zeroorder = false; kwargs...)
    # TODO update for new specs from Chris
    hoehalfwidth = 50.0 #25.0
    hoehalfheight = 50.0 #22.5
    hoecenter = SVector(-8.0 - 25.0, 0.0, -10.0 - 25.0)
    rect = Rectangle(hoehalfheight, hoehalfwidth, SVector(0.0, 1.0, 0.0), hoecenter)
    er = 15.0
    cornea_rad = 7.85
    corneavertex = SVector(0.0, er, 0.0)
    sourceloc = SVector(-33.0, er, 0.0)
    camloc = SVector(20.0, 3.0, -11.0)
    camdir = corneavertex - camloc
    camdir_norm = normalize(camdir)

    interfaces = []

    # offset = SVector(-5.0, 10.0, -10.0)
    # for θ in 0:(π / 6):(2π)
    #     ledloc = SVector(20 * cos(θ) + offset[1], 0 + offset[2], 15 * sin(θ) + offset[3])
    #     int = HologramInterface(ledloc, ConvergingBeam, sourceloc, DivergingBeam, 0.78, 100.0, Air, Examples_N_BK7, Air, Air, Air, 0.05, zeroorder)
    #     push!(interfaces, int)
    # end

    dirs = [SVector(0.7713, 0.6350, -0.0437), SVector(0.5667, 0.8111, -0.1445), SVector(0.3400, 0.9349, -0.1017), SVector(0.1492, 0.9878, 0.0445), SVector(0.0249, 0.9686, 0.2474), SVector(-0.0184, 0.8855, 0.4643), SVector(0.0254, 0.7537, 0.6567), SVector(0.1548, 0.5964, 0.7876), SVector(0.3570, 0.4462, 0.8207), SVector(0.5959, 0.3470, 0.7242), SVector(0.7976, 0.3449, 0.4948), SVector(0.8680, 0.4555, 0.1978)]

    for d in dirs
        int = HologramInterface(normalize(d), CollimatedBeam, sourceloc, DivergingBeam, 0.78, 100.0, Air, Examples_N_BK7, Air, Air, Air, 0.05, zeroorder)
        # int = HologramInterface(corneavertex - 10 * d, ConvergingBeam, sourceloc, DivergingBeam, 0.78, 100.0, Air, Examples_N_BK7, Air, Air, Air, 0.05, zeroorder)
        push!(interfaces, int)
    end

    mint = MultiHologramInterface(interfaces...)
    obj = MultiHologramSurface(rect, mint)
    cornea = leaf(Sphere(cornea_rad, interface = FresnelInterface{Float64}(EYE.CORNEA, Air, reflectance = 1.0, transmission = 0.0)), translation(0.0, er + cornea_rad, 0.0))()

    # cam settings
    fnum = 2.0
    fov = 80
    sensorrad = 1.0
    barrellength = sensorrad / tand(fov / 2)
    aprad = barrellength / fnum / 2
    camrad = max(sensorrad, aprad)

    camap = Annulus(aprad, camrad, camdir_norm, camloc)
    distfromcamtoeye = norm(camdir)
    focallength = 1 / (1 / distfromcamtoeye + 1 / barrellength)
    camlens = ParaxialLensEllipse(focallength, aprad, aprad, -camdir_norm, camloc)
    barrelloc = camloc - barrellength / 2 * camdir_norm
    barreltop = Plane(camdir_norm, camloc)
    barrelbot = Plane(-camdir_norm, camloc - 3 * barrellength * camdir_norm)
    barrelrot = OpticSim.rotmatbetween(SVector(0.0, 0.0, 1.0), camdir_norm)
    cambarrel = (
        barrelbot ∩
        barreltop ∩
        leaf(Cylinder(camrad, barrellength, interface = opaqueinterface(Float64)), Transform(barrelrot, barrelloc))
    )()
    camdet = Circle(sensorrad, camdir_norm, camloc - barrellength * camdir_norm, interface = opaqueinterface(Float64))

    # sourceleft = hoecenter[1] + hoehalfwidth - sourceloc[1]
    # sourceright = hoecenter[1] - hoehalfwidth - sourceloc[1]
    # sourceleftθ = atan(sourceleft, sourceloc[2])
    # sourcerightθ = atan(sourceright, sourceloc[2])
    # midθ = (sourceleftθ + sourcerightθ) / 2
    # sourcedir = normalize(SVector(er * tan(midθ), -er, 0.0))
    # sourceextentθ = abs(midθ - sourcerightθ)
    # source = CosineOpticalSource(RandomSource(OriginPoint{Float64}(1, position = sourceloc, direction = sourcedir), nrays, sourceextentθ), 1.0, 0.78)

    rays = Vector{OpticalRay{Float64,3}}(undef, nrays)
    @simd for i in 1:nrays
        p = point(rect, rand() * 2 - 1, rand() * 2 - 1)
        rays[i] = OpticalRay(sourceloc, p - sourceloc, 1.0, 0.78)
    end
    source = RayListSource(rays)

    sys = CSGOpticalSystem(LensAssembly(obj, cornea, camlens, cambarrel, camap), camdet, 800, 800)
    if det
        Vis.show(OpticSim.traceMT(sys, source))
    else
        Vis.drawtracerays(sys; raygenerator = source, trackallrays = true, kwargs...)
        # for θ in 0:(π / 6):(2π)
        #     ledloc = SVector(20 * cos(θ) + offset[1], 0 + offset[2], 15 * sin(θ) + offset[3])
        #     Vis.draw!(leaf(Sphere(1.0), translation(ledloc...)), color = :red)
        # end
        for d in dirs
            # Vis.draw!(leaf(Sphere(1.0), translation((corneavertex - 10 * d)...)), color = :red)
            Vis.draw!((corneavertex - 50 * d, corneavertex), color = :red)
        end
        if showhead
            Vis.draw!(joinpath(@__DIR__, "../../OBJ/glasses.obj"), scale = 100.0, transform = Transform(OpticSim.rotmatd(90, 0, 0), [27.0, 45.0, -8.0]), color = :black)
            Vis.draw!(joinpath(@__DIR__, "../../OBJ/femalehead.obj"), scale = 13.0, transform = Transform(OpticSim.rotmatd(0, 0, 180), [27.0, 105.0, -148.0]), color = :white)
        end
        Vis.display()
    end
end


using Dates
using ManpowerPlanning
using ManpowerPlanningIO
using Plots
using Test
using XLSX

versionMPIO()
println()

MPsim = ManpowerSimulation
MPIO = ManpowerPlanningIO

tStart = now()

@testset "Manpower Simulation I/O tests" begin

@testset "Excel configuration tests" begin

include( "base.jl" )
include( "attrition.jl" )
include( "attribute.jl" )
include( "basenode.jl" )
include( "compoundnode.jl" )
include( "recruitment.jl" )
include( "transition.jl" )
include( "retirement.jl" )
include( "initpop.jl" )

end  # @testset "Excel configuration tests"

@testset "Excel run tests" begin

include( "randomseed.jl" )

end  # @testset "Excel run tests"

@testset "Database configuration tests" begin

include( "db/complete.jl" )

end  # @testset "Database configuration tests" begin

include( "report/simprep.jl" )

@testset "Simulation report tests" begin

rm.( string.( "report/", filter( fname -> endswith( fname, ".xlsx" ),
    readdir( "report" ) ) ) )
include( "report/transitionflux.jl" )
include( "report/nodeflux.jl" )
include( "report/nodepop.jl" )
include( "report/nodecomposition.jl" )
include( "report/subpop.jl" )
include( "report/initpop.jl" )

end  # @testset "Simulation report tests"

@testset "Simulation plot tests" begin

gui( plot() )

rm.( string.( "plot/", filter( fname -> endswith( fname, ".svg" ),
    readdir( "plot" ) ) ) )
include( "plot/transitionflux.jl" )
include( "plot/nodeflux.jl" )
include( "plot/nodepop.jl" )
include( "plot/nodecomposition.jl" )
include( "plot/subpop.jl" )
include( "plot/initpop.jl" )

end  # @testset "Simulation plot tests"

@testset "Excel plot request tests" begin

rm( "excelrequest/plotRequest", recursive=true, force=true )
include( "excelrequest/transitionflux.jl" )
include( "excelrequest/nodepop.jl" )
include( "excelrequest/initpop.jl" )

end  # @testset "Excel plot request tests"

end  # @testset "Manpower Simulation I/O tests"

tElapsed = ( now() - tStart ).value / 1000
@info string( "Unit tests completed in ", tElapsed, " seconds." )


# This file is a part of Julia. License is MIT: https://julialang.org/license

module RoundingTests

using Test
using Dates

@testset "conversion to and from the rounding epoch (ISO 8601 year 0000)" begin
    @test Dates.epochdays2date(-1) == Dates.Date(-1, 12, 31)
    @test Dates.epochdays2date(0) == Dates.Date(0, 1, 1)
    @test Dates.epochdays2date(1) == Dates.Date(0, 1, 2)
    @test Dates.epochdays2date(736329) == Dates.Date(2016, 1, 1)
    @test Dates.epochms2datetime(-86400000) == Dates.DateTime(-1, 12, 31)
    @test Dates.epochms2datetime(0) == Dates.DateTime(0, 1, 1)
    @test Dates.epochms2datetime(86400000) == Dates.DateTime(0, 1, 2)
    @test Dates.epochms2datetime(Int64(736329) * 86400000) == Dates.DateTime(2016, 1, 1)
    @test Dates.date2epochdays(Dates.Date(-1, 12, 31)) == -1
    @test Dates.date2epochdays(Dates.Date(0, 1, 1)) == 0
    @test Dates.date2epochdays(Dates.Date(2016, 1, 1)) == 736329
    @test Dates.datetime2epochms(Dates.DateTime(-1, 12, 31)) == -86400000
    @test Dates.datetime2epochms(Dates.DateTime(0, 1, 1)) == 0
    @test Dates.datetime2epochms(Dates.DateTime(2016, 1, 1)) == Int64(736329) * 86400000
end
@testset "Basic rounding" begin
    dt = Dates.Date(2016, 2, 28)    # Sunday
    @test floor(dt, Dates.Year) == Dates.Date(2016)
    @test floor(dt, Dates.Year(5)) == Dates.Date(2015)
    @test floor(dt, Dates.Year(10)) == Dates.Date(2010)
    @test floor(dt, Dates.Quarter) == Dates.Date(2016, 1)
    @test floor(dt, Dates.Quarter(6)) == Dates.Date(2016, 1)
    @test floor(dt, Dates.Month) == Dates.Date(2016, 2)
    @test floor(dt, Dates.Month(6)) == Dates.Date(2016, 1)
    @test floor(dt, Dates.Week) == Dates.toprev(dt, Dates.Monday)
    @test ceil(dt, Dates.Year) == Dates.Date(2017)
    @test ceil(dt, Dates.Year(5)) == Dates.Date(2020)
    @test ceil(dt, Dates.Quarter) == Dates.Date(2016, 4)
    @test ceil(dt, Dates.Quarter(6)) == Dates.Date(2017, 7)
    @test ceil(dt, Dates.Month) == Dates.Date(2016, 3)
    @test ceil(dt, Dates.Month(6)) == Dates.Date(2016, 7)
    @test ceil(dt, Dates.Week) == Dates.tonext(dt, Dates.Monday)
    @test round(dt, Dates.Year) == Dates.Date(2016)
    @test round(dt, Dates.Month) == Dates.Date(2016, 3)
    @test round(dt, Dates.Week) == Dates.Date(2016, 2, 29)

    dt = Dates.DateTime(2016, 2, 28, 15, 10, 50, 500)
    @test floor(dt, Dates.Day) == Dates.DateTime(2016, 2, 28)
    @test floor(dt, Dates.Hour) == Dates.DateTime(2016, 2, 28, 15)
    @test floor(dt, Dates.Hour(2)) == Dates.DateTime(2016, 2, 28, 14)
    @test floor(dt, Dates.Hour(12)) == Dates.DateTime(2016, 2, 28, 12)
    @test floor(dt, Dates.Minute) == Dates.DateTime(2016, 2, 28, 15, 10)
    @test floor(dt, Dates.Minute(15)) == Dates.DateTime(2016, 2, 28, 15, 0)
    @test floor(dt, Dates.Second) == Dates.DateTime(2016, 2, 28, 15, 10, 50)
    @test floor(dt, Dates.Second(30)) == Dates.DateTime(2016, 2, 28, 15, 10, 30)
    @test ceil(dt, Dates.Day) == Dates.DateTime(2016, 2, 29)
    @test ceil(dt, Dates.Hour) == Dates.DateTime(2016, 2, 28, 16)
    @test ceil(dt, Dates.Hour(2)) == Dates.DateTime(2016, 2, 28, 16)
    @test ceil(dt, Dates.Hour(12)) == Dates.DateTime(2016, 2, 29, 0)
    @test ceil(dt, Dates.Minute) == Dates.DateTime(2016, 2, 28, 15, 11)
    @test ceil(dt, Dates.Minute(15)) == Dates.DateTime(2016, 2, 28, 15, 15)
    @test ceil(dt, Dates.Second) == Dates.DateTime(2016, 2, 28, 15, 10, 51)
    @test ceil(dt, Dates.Second(30)) == Dates.DateTime(2016, 2, 28, 15, 11, 0)
    @test round(dt, Dates.Day) == Dates.DateTime(2016, 2, 29)
    @test round(dt, Dates.Hour) == Dates.DateTime(2016, 2, 28, 15)
    @test round(dt, Dates.Hour(2)) == Dates.DateTime(2016, 2, 28, 16)
    @test round(dt, Dates.Hour(12)) == Dates.DateTime(2016, 2, 28, 12)
    @test round(dt, Dates.Minute) == Dates.DateTime(2016, 2, 28, 15, 11)
    @test round(dt, Dates.Minute(15)) == Dates.DateTime(2016, 2, 28, 15, 15)
    @test round(dt, Dates.Second) == Dates.DateTime(2016, 2, 28, 15, 10, 51)
    @test round(dt, Dates.Second(30)) == Dates.DateTime(2016, 2, 28, 15, 11, 0)
end
@testset "Rounding for dates at the rounding epoch (year 0000)" begin
    dt = Dates.DateTime(0)
    @test floor(dt, Dates.Year) == dt
    @test floor(dt, Dates.Month) == dt
    @test floor(dt, Dates.Week) == Dates.Date(-1, 12, 27)   # Monday prior to 0000-01-01
    @test floor(Dates.Date(-1, 12, 27), Dates.Week) == Dates.Date(-1, 12, 27)
    @test floor(dt, Dates.Day) == dt
    @test floor(dt, Dates.Hour) == dt
    @test floor(dt, Dates.Minute) == dt
    @test floor(dt, Dates.Second) == dt
    @test ceil(dt, Dates.Year) == dt
    @test ceil(dt, Dates.Month) == dt
    @test ceil(dt, Dates.Week) == Dates.Date(0, 1, 3)       # Monday following 0000-01-01
    @test ceil(Dates.Date(0, 1, 3), Dates.Week) == Dates.Date(0, 1, 3)
    @test ceil(dt, Dates.Day) == dt
    @test ceil(dt, Dates.Hour) == dt
    @test ceil(dt, Dates.Minute) == dt
    @test ceil(dt, Dates.Second) == dt
end
@testset "Rounding for multiples of a period" begin
    # easiest to test close to rounding epoch
    dt = Dates.DateTime(0, 1, 19, 19, 19, 19, 19)
    @test floor(dt, Dates.Year(2)) == DateTime(0)
    @test floor(dt, Dates.Month(2)) == DateTime(0, 1)       # Odd number; months are 1-indexed
    @test floor(dt, Dates.Week(2)) == DateTime(0, 1, 17)    # Third Monday of 0000
    @test floor(dt, Dates.Day(2)) == DateTime(0, 1, 19)     # Odd number; days are 1-indexed
    @test floor(dt, Dates.Hour(2)) == DateTime(0, 1, 19, 18)
    @test floor(dt, Dates.Minute(2)) == DateTime(0, 1, 19, 19, 18)
    @test floor(dt, Dates.Second(2)) == DateTime(0, 1, 19, 19, 19, 18)
    @test ceil(dt, Dates.Year(2)) == DateTime(2)
    @test ceil(dt, Dates.Month(2)) == DateTime(0, 3)        # Odd number; months are 1-indexed
    @test ceil(dt, Dates.Week(2)) == DateTime(0, 1, 31)     # Fifth Monday of 0000
    @test ceil(dt, Dates.Day(2)) == DateTime(0, 1, 21)      # Odd number; days are 1-indexed
    @test ceil(dt, Dates.Hour(2)) == DateTime(0, 1, 19, 20)
    @test ceil(dt, Dates.Minute(2)) == DateTime(0, 1, 19, 19, 20)
    @test ceil(dt, Dates.Second(2)) == DateTime(0, 1, 19, 19, 19, 20)
end
@testset "Rounding for dates with negative years" begin
    dt = Dates.DateTime(-1, 12, 29, 19, 19, 19, 19)
    @test floor(dt, Dates.Year(2)) == DateTime(-2)
    @test floor(dt, Dates.Month(2)) == DateTime(-1, 11)     # Odd number; months are 1-indexed
    @test floor(dt, Dates.Week(2)) == DateTime(-1, 12, 20)  # 2 weeks prior to 0000-01-03
    @test floor(dt, Dates.Day(2)) == DateTime(-1, 12, 28)   # Even; 4 days prior to 0000-01-01
    @test floor(dt, Dates.Hour(2)) == DateTime(-1, 12, 29, 18)
    @test floor(dt, Dates.Minute(2)) == DateTime(-1, 12, 29, 19, 18)
    @test floor(dt, Dates.Second(2)) == DateTime(-1, 12, 29, 19, 19, 18)
    @test ceil(dt, Dates.Year(2)) == DateTime(0)
    @test ceil(dt, Dates.Month(2)) == DateTime(0, 1)        # Odd number; months are 1-indexed
    @test ceil(dt, Dates.Week(2)) == DateTime(0, 1, 3)      # First Monday of 0000
    @test ceil(dt, Dates.Day(2)) == DateTime(-1, 12, 30)    # Even; 2 days prior to 0000-01-01
    @test ceil(dt, Dates.Hour(2)) == DateTime(-1, 12, 29, 20)
    @test ceil(dt, Dates.Minute(2)) == DateTime(-1, 12, 29, 19, 20)
    @test ceil(dt, Dates.Second(2)) == DateTime(-1, 12, 29, 19, 19, 20)
end
@testset "Rounding for dates that should not need rounding" begin
    for dt in [Dates.DateTime(2016, 1, 1), Dates.DateTime(-2016, 1, 1)]
        local dt
        for p in [Dates.Year, Dates.Month, Dates.Day, Dates.Hour, Dates.Minute, Dates.Second]
            local p
            @test floor(dt, p) == dt
            @test ceil(dt, p) == dt
        end
    end
end
@testset "Various available RoundingModes" begin
    dt = Dates.DateTime(2016, 2, 28, 12)
    @test round(dt, Dates.Day, RoundNearestTiesUp) == Dates.DateTime(2016, 2, 29)
    @test round(dt, Dates.Day, RoundUp) == Dates.DateTime(2016, 2, 29)
    @test round(dt, Dates.Day, RoundDown) == Dates.DateTime(2016, 2, 28)
    @test_throws DomainError round(dt, Dates.Day, RoundNearest)
    @test_throws DomainError round(dt, Dates.Day, RoundNearestTiesAway)
    @test_throws DomainError round(dt, Dates.Day, RoundToZero)
    @test round(dt, Dates.Day) == round(dt, Dates.Day, RoundNearestTiesUp)
end
@testset "Rounding datetimes to invalid resolutions" begin
    dt = Dates.DateTime(2016, 2, 28, 12, 15)
    for p in [Dates.Year, Dates.Month, Dates.Week, Dates.Day, Dates.Hour]
        local p
        for v in [-1, 0]
            @test_throws DomainError floor(dt, p(v))
            @test_throws DomainError ceil(dt, p(v))
            @test_throws DomainError round(dt, p(v))
        end
    end
end
@testset "Rounding for periods" begin
    x = Dates.Second(172799)
    @test floor(x, Dates.Week) == Dates.Week(0)
    @test floor(x, Dates.Day) == Dates.Day(1)
    @test floor(x, Dates.Hour) == Dates.Hour(47)
    @test floor(x, Dates.Minute) == Dates.Minute(2879)
    @test floor(x, Dates.Second) == Dates.Second(172799)
    @test floor(x, Dates.Millisecond) == Dates.Millisecond(172799000)
    @test ceil(x, Dates.Week) == Dates.Week(1)
    @test ceil(x, Dates.Day) == Dates.Day(2)
    @test ceil(x, Dates.Hour) == Dates.Hour(48)
    @test ceil(x, Dates.Minute) == Dates.Minute(2880)
    @test ceil(x, Dates.Second) == Dates.Second(172799)
    @test ceil(x, Dates.Millisecond) == Dates.Millisecond(172799000)
    @test round(x, Dates.Week) == Dates.Week(0)
    @test round(x, Dates.Day) == Dates.Day(2)
    @test round(x, Dates.Hour) == Dates.Hour(48)
    @test round(x, Dates.Minute) == Dates.Minute(2880)
    @test round(x, Dates.Second) == Dates.Second(172799)
    @test round(x, Dates.Millisecond) == Dates.Millisecond(172799000)

    x = Dates.Nanosecond(2000999999)
    @test floor(x, Dates.Second) == Dates.Second(2)
    @test floor(x, Dates.Millisecond) == Dates.Millisecond(2000)
    @test floor(x, Dates.Microsecond) == Dates.Microsecond(2000999)
    @test floor(x, Dates.Nanosecond) == x
    @test ceil(x, Dates.Second) == Dates.Second(3)
    @test ceil(x, Dates.Millisecond) == Dates.Millisecond(2001)
    @test ceil(x, Dates.Microsecond) == Dates.Microsecond(2001000)
    @test ceil(x, Dates.Nanosecond) == x
    @test round(x, Dates.Second) == Dates.Second(2)
    @test round(x, Dates.Millisecond) == Dates.Millisecond(2001)
    @test round(x, Dates.Microsecond) == Dates.Microsecond(2001000)
    @test round(x, Dates.Nanosecond) == x
end
@testset "Rounding for periods that should not need rounding" begin
    for x in [Dates.Week(3), Dates.Day(14), Dates.Second(604800)]
        local x
        for p in [Dates.Week, Dates.Day, Dates.Hour, Dates.Second, Dates.Millisecond, Dates.Microsecond, Dates.Nanosecond]
            local p
            @test floor(x, p) == p(x)
            @test ceil(x, p) == p(x)
        end
    end
end
@testset "Various available RoundingModes for periods" begin
    x = Dates.Hour(36)
    @test round(x, Dates.Day, RoundNearestTiesUp) == Dates.Day(2)
    @test round(x, Dates.Day, RoundUp) == Dates.Day(2)
    @test round(x, Dates.Day, RoundDown) == Dates.Day(1)
    @test_throws DomainError round(x, Dates.Day, RoundNearest)
    @test_throws DomainError round(x, Dates.Day, RoundNearestTiesAway)
    @test_throws DomainError round(x, Dates.Day, RoundToZero)
    @test round(x, Dates.Day) == round(x, Dates.Day, RoundNearestTiesUp)
end
@testset "Rounding periods to invalid resolutions" begin
    x = Dates.Hour(86399)
    for p in [Dates.Week, Dates.Day, Dates.Hour, Dates.Second, Dates.Millisecond, Dates.Microsecond, Dates.Nanosecond]
        local p
        for v in [-1, 0]
            @test_throws DomainError floor(x, p(v))
            @test_throws DomainError ceil(x, p(v))
            @test_throws DomainError round(x, p(v))
        end
    end
    for p in [Dates.Year, Dates.Month]
        local p
        for v in [-1, 0, 1]
            @test_throws MethodError floor(x, p(v))
            @test_throws MethodError ceil(x, p(v))
            @test_throws DomainError round(x, p(v))
        end
    end
end

end


function tr4!(r::Robot)#главная функция
    move_corner(r)#перемещаем робота в угол
    mark!(r)
end
function mark!(r::Robot)
    for i in 1:11
        for i in i:11
            if isborder(r, Ost) == false
                putmarker!(r)
                move!(r, Ost)
            end
        end
        putmarker!(r)
        for i in i:11
            if isborder(r, West) == false
                move!(r, West)
            end
        end
        if isborder(r,Nord) == false
            move!(r,Nord)
        end
        i=i+1
    end
end
function move_corner(r::Robot) #перемещаем робота в угол
    while isborder(r, West) == false
        move!(r, West)
    end
    while isborder(r, Sud) == false
        move!(r, Sud)
    end
end


#
# TODO: should ITensorMap be a special version of
# an ITensorNetwork with input and output indices specified?
#

# T is however the nodes are indexed
# TODO: how to deal with 2D, multiple networks, etc.?
# struct IndexSetNetwork{T}
#   # Use Vector{SortedVector{Pair{T, IndexSet}}}
#   data::Vector{Vector{Pair{T, IndexSet}}}
# end

# Make MPS, PEPS, etc. wrappers around `ITensorNetwork`,
# which could be subclasses of `AbstractITensorNetwork`
#
# Also `InfiniteITensorNetwork`, `AbstractInfiniteITensorNetwork`:
#
# struct InfiniteMPS <: AbstractInfiniteITensorNetwork
#   tensornetwork::InfiniteITensorNetwork
# end
#
# struct ITensorNetwork <: AbstractITensorNetwork
#   itensors::Vector{ITensor}
#   indnetwork::IndexSetNetwork # Adjacency list of IndexSets
#   input_inds::IndexSet
#   output_inds::IndexSet
# end
#
# TODO: how to deal with networks of ITensorNetwork,
# for example a network of MPS and MPO?
# ITensorNetworkNetwork that is a tree of ITensorNetwork?
#

abstract type AbstractITensorMap end

input_inds(T::AbstractITensorMap) = T.input_inds
output_inds(T::AbstractITensorMap) = T.output_inds
(T::AbstractITensorMap)(v::ITensor) = replaceinds(T * v, output_inds(T) => input_inds(T))

# convert from Tuple to Vector
tuple_to_vector(t::Tuple) = collect(t)
tuple_to_vector(v::Vector) = v

# Represents the action of applying the
# vector of ITensors to a starting state and then mapping
# them back (from output_inds to input_inds)
# TODO: rename ITensorNetworkMap?
# TODO: maybe parametrize the type to allow storing just 1 ITensor?
# TODO: contraction order optimization!
struct ITensorMap <: AbstractITensorMap
  itensors::Vector{ITensor}
  scalar::Number
  input_inds::Vector{Index}
  output_inds::Vector{Index}
  function ITensorMap(itensors::Vector{ITensor}, scalar, input_inds, output_inds)
    inds_in = tuple_to_vector(input_inds)
    inds_out = tuple_to_vector(output_inds)
    #inds_eltype = promote_type(eltype(input_inds), eltype(output_inds))
    #return new{inds_eltype}(itensors, inds_in, inds_out)
    return new(itensors, scalar, inds_in, inds_out)
  end
end

function ITensorMap(itensors::Vector{ITensor}, input_inds, output_inds)
  return ITensorMap(itensors, true, input_inds, output_inds)
end

function (M1::ITensorMap * M2::ITensorMap)
  # TODO: check the directions are correct
  @assert output_inds(M2) == input_inds(M1)
  return ITensorMap(
    vcat(M1.itensors, M2.itensors), M1.scalar * M2.scalar, input_inds(M2), output_inds(M1)
  )
end

function default_input_inds(itensors::Vector{ITensor})
  return filter(i -> plev(i) == 0, noncommoninds(itensors...))
end

function ITensorMap(
  itensors::Vector{ITensor};
  input_inds=default_input_inds(itensors),
  output_inds=dag(input_inds'),
)
  return ITensorMap(itensors, input_inds, output_inds)
end

Base.iterate(T::ITensorMap, args...) = iterate(T.itensors, args...)

function Base.transpose(T::ITensorMap)
  return ITensorMap(reverse(T.itensors), output_inds(T), input_inds(T))
end

# This is actually a Hermitian conjugation, not priming
function Base.adjoint(T::ITensorMap)
  return ITensorMap(reverse(dag.(T.itensors)), dag(output_inds(T)), dag(input_inds(T)))
end

# TODO: make a default constructor that searches for pairs of primed and unprimed indices
# TODO: this would be useful for ITensor matrix factorizations!
# function ITensorMap(itensors::Vector{ITensor}, pair_match::Pair = 0 => 1)
#   # pair_match could be:
#   # pair_match = 0 => 2
#   # pair_match = "Input" => "Output"
#   # pair_match = ("Input", 0) => ("Output", 1)
#   external_inds = unioninds(siteinds(itensors)...)
#   input_match = first(pair_match)
#   output_match = first(pair_match)
#   # TODO: preprocess pair_match to be of the form `(tags, plev)`
#   ind_match(i::Index, match) = hastags(i, match[1]) && hasplev(i, match[2])
#   input_inds = filter(i -> ind_match(i, input_match), external_inds)
#   output_inds = dag(replaceprime(replacetags(input_inds, first.(pair_match)), last.(pair_match)))
#   @assert = hassameinds(output_inds, filter(i -> ind_match(i, output_match), external_inds))
#   return ITensorMap(itensors, input_inds, output_inds)
# end

function set_scalar(T::ITensorMap, scalar::Number)
  return ITensorMap(T.itensors, scalar, input_inds(T), output_inds(T))
end

# Lazily scale by a scalar
(T::ITensorMap * c::Number) = set_scalar(T, T.scalar * c)
(c::Number * T::ITensorMap) = set_scalar(T, c * T.scalar)

-(T::ITensorMap) = set_scalar(T, -T.scalar)

# TODO: assert isempty(uniqeinds(v, T))?
# Apply the operator as T|v⟩
(T::ITensorMap * v::ITensor) = T.scalar * contract(pushfirst!(copy(T.itensors), v)) #*(v, T...)
# Apply the operator as ⟨v̅|T (simple left multiplication, without conjugation)
# This applies the ITensorMap tensors in reverse (maybe this is not always the best contraction
# ordering)
(v::ITensor * T::ITensorMap) = T.scalar * contract(pushfirst!(reverse(T.itensors, v))) #*(v, reverse(T)...)

# TODO: implement Base.iterate(::Base.Iterators.Reverse{MPS})

# TODO: use something like:
# neighbors(ψ, ϕ, (1, 1))
# neighbors(ψ, ϕ, (2, 1))
# neighbors(ψ, ϕ, (1, N))
# neighbors(ψ, ϕ, (2, N))
# Transfer matrix made from two MPS: T|v⟩ -> |w⟩
function ITensorMap(ψ::MPS, ϕ::MPS; input_inds=nothing, output_inds=nothing)
  N = length(ψ)
  @assert length(ϕ) == N
  itensors::Vector{ITensor} = reverse(collect(Iterators.flatten(Iterators.zip(ψ, ϕ))))
  if isnothing(input_inds)
    input_inds = unioninds(
      uniqueinds(ψ[N], ψ[N - 1], ϕ[N]), uniqueinds(ϕ[N], ϕ[N - 1], ψ[N])
    )
  end
  if isnothing(output_inds)
    output_inds = unioninds(uniqueinds(ψ[1], ψ[2], ϕ[1]), uniqueinds(ϕ[1], ϕ[2], ψ[1]))
  end
  return ITensorMap(itensors, input_inds, output_inds)
end

# Represents a sum of ITensor maps
struct ITensorMapSum <: AbstractITensorMap
  itensormaps::Vector{ITensorMap}
  input_inds::Vector{Index}
  output_inds::Vector{Index}
  function ITensorMapSum(itensormaps::Vector{ITensorMap})
    # TODO: check that all input_inds and output_inds are the same
    return new(itensormaps, input_inds(first(itensormaps)), output_inds(first(itensormaps)))
  end
end

(M1::ITensorMap + M2::ITensorMap) = ITensorMapSum([M1, M2])
(M1::ITensorMapSum + M2::ITensorMap) = ITensorMapSum(push!(copy(M1.itensormaps), M2))
(M1::ITensorMap + M2::ITensorMapSum) = M2 + M1
(M1::ITensorMap - M2::ITensorMap) = M1 + (-M2)
(M1::ITensorMapSum - M2::ITensorMap) = M1 + (-M2)
(M1::ITensorMap - M2::ITensorMapSum) = M1 + (-M2)
(M::ITensorMapSum * v::ITensor) = sum([m * v for m in M.itensormaps])

(M::ITensorMapSum * c::Number) = ITensorMapSum([m * c for m in M.itensormaps])
(c::Number * M::ITensorMapSum) = M * c
-(M::ITensorMapSum) = -1 * M

# Required by Arpack.eigs
Base.size(T::AbstractITensorMap) = (dim(output_inds(T)), dim(input_inds(T)))
LinearAlgebra.issymmetric(::AbstractITensorMap) = false
# TODO: promote the element types of all of the ITensors
# in the ITensorMap
Base.eltype(T::AbstractITensorMap) = eltype(T.itensors[1])
function LinearAlgebra.mul!(y, A::AbstractITensorMap, x)
  xt = itensor(x, dag(input_inds(A)); tol=1e-15)
  # XXX: something wrong with in-place ITensor
  # contraction, not overwriting input data
  yt = A(xt)
  yt = permute(yt, input_inds(A))
  y .= vec(array(yt))
  return y
end


"""
The `ViscousFlow` module is here

"""
module ViscousFlow

using Reexport

#== Imports/Exports ==#

include("Utils.jl")
@reexport using .Utils

include("Fields.jl")

@reexport using .Fields

include("Bodies.jl")

@reexport using .Bodies

include("RigidBodyMotions.jl")

@reexport using .RigidBodyMotions

include("Layers.jl")

@reexport using .Layers

include("SaddlePointSystems.jl")

@reexport using .SaddlePointSystems

include("TimeMarching.jl")
@reexport using .TimeMarching

include("Systems.jl")
@reexport using .Systems

#== Plot Recipes ==#

include("plot_recipes.jl")

end


importall ImmutableArrays

typealias GLboolean  Bool
typealias GLbyte     Int8
typealias GLubyte    Uint8
typealias GLchar     Uint8
typealias GLshort    Int16
typealias GLushort   Uint16
typealias GLint      Int32
typealias GLuint     Uint32
typealias GLint64    Int64
typealias GLuint64   Uint64
typealias GLsizei    Uint32
typealias GLenum     Uint32
typealias GLintptr   Ptr
typealias GLsizeiptr Int
typealias GLfloat    Float32
typealias GLdouble   Float64
typealias GLvoid     Void

immutable Object
	handle::GLuint
end
Object(x) = Object(convert(GLuint,x))

immutable Shader
	handle::Object
end
Shader(x) = Shader(Object(x))

immutable Program
	handle::Object
end
Program(x) = Program(Object(x))

typealias Buffer      Object
typealias Texture     Object
typealias VertexArray Object

typealias Attribute GLint

immutable Uniform
	location::GLint
end
Uniform(location) = Uniform(convert(GLint, location))
+(u::Uniform, i::Integer) = Uniform(u.location+i)

typealias Vec2 Vector2{Float32}
typealias Vec3 Vector3{Float32}
typealias Vec4 Vector4{Float32}


abstract type Edge{N,T} end
Base.broadcastable(e::Edge) = Ref(e)
Base.@propagate_inbounds function Base.getindex(e::Edge, i::Int)
    getfield(e, :points)[i]
end


import Base: any, all, count, countnz

#############################
# reduce
# functions in base implemented with a direct loop need to be overloaded to use mapreduce
any(pred, A::GPUArray) = Bool(mapreduce(pred, |, Int32(0), A))
all(pred, A::GPUArray) = Bool(mapreduce(pred, &, Int32(1), A))
count(pred, A::GPUArray) = Int(mapreduce(pred, +, UInt32(0), A))
countnz(A::GPUArray) = Int(mapreduce(x-> x != 0, +, UInt32(0), A))
countnz(A::GPUArray, dim) = Int(mapreducedim(x-> x != 0, +, UInt32(0), A, dim))

Base.:(==)(A::GPUArray, B::GPUArray) = Bool(mapreduce(==, &, Int32(1), A, B))

# hack to get around of fetching the first element of the GPUArray
# as a startvalue, which is a bit complicated with the current reduce implementation
function startvalue(f, T)
    error("Please supply a starting value for mapreduce. E.g: mapreduce($f, $op, 1, A)")
end
startvalue(::typeof(+), T) = zero(T)
startvalue(::typeof(*), T) = one(T)
startvalue(::typeof(Base.scalarmin), T) = typemax(T)
startvalue(::typeof(Base.scalarmax), T) = typemin(T)

# TODO widen and support Int64 and use Base.r_promote_type
gpu_promote_type(op, ::Type{T}) where {T} = T
gpu_promote_type(op, ::Type{T}) where {T<:Base.WidenReduceResult} = T
gpu_promote_type(::typeof(+), ::Type{T}) where {T<:Base.WidenReduceResult} = T
gpu_promote_type(::typeof(*), ::Type{T}) where {T<:Base.WidenReduceResult} = T
gpu_promote_type(::typeof(+), ::Type{T}) where {T<:Number} = typeof(zero(T)+zero(T))
gpu_promote_type(::typeof(*), ::Type{T}) where {T<:Number} = typeof(one(T)*one(T))
gpu_promote_type(::typeof(Base.scalarmax), ::Type{T}) where {T<:Base.WidenReduceResult} = T
gpu_promote_type(::typeof(Base.scalarmin), ::Type{T}) where {T<:Base.WidenReduceResult} = T
gpu_promote_type(::typeof(max), ::Type{T}) where {T<:Base.WidenReduceResult} = T
gpu_promote_type(::typeof(min), ::Type{T}) where {T<:Base.WidenReduceResult} = T

function Base.mapreduce(f::Function, op::Function, A::GPUArray{T, N}) where {T, N}
    OT = gpu_promote_type(op, T)
    v0 = startvalue(op, OT) # TODO do this better
    mapreduce(f, op, v0, A)
end
function acc_mapreduce end
function Base.mapreduce(f, op, v0, A::GPUArray, B::GPUArray, C::Number)
    acc_mapreduce(f, op, v0, A, (B, C))
end
function Base.mapreduce(f, op, v0, A::GPUArray, B::GPUArray)
    acc_mapreduce(f, op, v0, A, (B,))
end
function Base.mapreduce(f, op, v0, A::GPUArray)
    acc_mapreduce(f, op, v0, A, ())
end

@generated function mapreducedim_kernel(state, f, op, R, A, range::NTuple{N, Any}) where N
    types = (range.parameters...,)
    indices = ntuple(i-> Symbol("I_$i"), N)
    Iexpr = ntuple(i-> :(I[$i]), N)
    body = :(@inbounds R[$(Iexpr...)] = op(R[$(Iexpr...)], f(A[$(indices...)])))
    for i = N:-1:1
        idxsym = indices[i]
        if types[i] == Void
            body = quote
                $idxsym = I[$i]
                $body
            end
        else
            rsym = Symbol("r_$i")
            body = quote
                $(rsym) = range[$i]
                for $idxsym in UInt32(first($rsym)):UInt32(last($rsym))
                    $body
                end
            end
        end
        body
    end
    quote
        I = @cartesianidx R state
        $body
        return
    end
end

function Base._mapreducedim!(f, op, R::GPUArray, A::GPUArray)
    range = ifelse.(length.(indices(R)) .== 1, indices(A), nothing)
    gpu_call(mapreducedim_kernel, R, (f, op, R, A, range))
    return R
end

for i = 0:10
    args = ntuple(x-> Symbol("arg_", x), i)
    fargs = ntuple(x-> :(broadcast_index($(args[x]), length, global_index)), i)
    @eval begin
        # http://developer.amd.com/resources/articles-whitepapers/opencl-optimization-case-study-simple-reductions/
        function reduce_kernel(state, f, op, v0::T, A, ::Val{LMEM}, result, $(args...)) where {T, LMEM}
            ui0 = UInt32(0); ui1 = UInt32(1); ui2 = UInt32(2)
            tmp_local = @LocalMemory(state, T, LMEM)
            global_index = linear_index(state)
            acc = v0
            # # Loop sequentially over chunks of input vector
            while global_index <= length(A)
                element = f(A[global_index], $(fargs...))
                acc = op(acc, element)
                global_index += global_size(state)
            end
            # Perform parallel reduction
            local_index = threadidx_x(state) - ui1
            tmp_local[local_index + ui1] = acc
            synchronize_threads(state)

            offset = blockdim_x(state) ÷ ui2
            while offset > ui0
                if (local_index < offset)
                    other = tmp_local[local_index + offset + ui1]
                    mine = tmp_local[local_index + ui1]
                    tmp_local[local_index + ui1] = op(mine, other)
                end
                synchronize_threads(state)
                offset = offset ÷ ui2
            end
            if local_index == ui0
                result[blockidx_x(state)] = tmp_local[ui1]
            end
            return
        end
    end

end

to_cpu(x) = x
to_cpu(x::GPUArray) = Array(x)

function acc_mapreduce(
        f, op, v0::OT, A::GPUArray{T, N}, rest::Tuple
    ) where {T, OT, N}
    dev = device(A)
    blocksize = 80
    threads = 256
    if length(A) <= blocksize * threads
        args = zip(Array(A), to_cpu.(rest)...)
        return mapreduce(x-> f(x...), op, v0, args)
    end
    out = similar(A, OT, (blocksize,))
    fill!(out, v0)
    args = (f, op, v0, A, Val{threads}(), out, rest...)
    gpu_call(reduce_kernel, A, args, ((blocksize,), (threads,)))
    reduce(op, Array(out))
end


using StaticArrays
using CSV

"""
Represents an atom or molecule, irespective of its charge.
"""
struct Formula
    #supports up to triatomic molecules, can be trivially extended.
    #Unlike tuples SVectors support sorting
    atoms::SVector{3, UInt8}

    function Formula(Z::Integer)
        @assert 1 <= Z <= Natoms
        new([0x00, 0x00, Z])
    end
    function Formula(Zs::AbstractVector{<:Integer})
        l = length(Zs)
        if l == 0
            throw(ArgumentError("Can't construct an empty Formula"))
        elseif l == 1
            Zs = [0 ; 0 ; Zs]
        elseif l == 2
            Zs = [0 ; Zs]
        elseif l > 3
            throw(ArgumentError("Can't construct Formula with more than three atoms"))
        end
        @assert(issorted(Zs))
        new(Zs)
    end

    """
        Formula(code::String)

        Construct a Formula from an encoded string form.  This can be a MOOG-style numeric code, i.e.
        "0801" for OH, or an atomic or molecular symbol, i.e. "FeH", "Li", or "C2".
    """
    function Formula(code::AbstractString)
        if code in atomic_symbols #quick-parse single elements
            return new([0, 0, atomic_numbers[code]])
        end

        #handle numeric codes, e.g. 0801 -> OH
        if all(isdigit(c) for c in code)
            if length(code) <= 2
                return Formula(parse(Int,code))
            elseif length(code) <= 4
                if length(code) == 3
                    code = "0"*code
                end
                el1 = parse(Int, code[1:2])
                el2 = parse(Int, code[3:4])
                return new([0x00, min(el1, el2), max(el1, el2)])
            else
                throw(ArgumentError("numeric codes for molecules with more than 4 chars like " *
                                    "$(code) are not supported"))
            end
        end
        #otherwise, code should be "OH", "FeH", "Li", "C2", etc.
        inds = filter(1:length(code)) do i
            isdigit(code[i]) || isuppercase(code[i])
        end
        push!(inds, length(code)+1)
        subcode = map(1:(length(inds)-1)) do j
            code[inds[j]:inds[j+1]-1]
        end

        atoms = UInt8[]
        for s in subcode
            num = tryparse(Int, s)
            if num isa Int
                previous = atoms[end]
                for _ in 1:(num-1)
                    push!(atoms, previous)
                end
            else
                push!(atoms, atomic_numbers[s])
            end
        end

        sort!(atoms)
        Formula(atoms)
    end
end

function get_atoms(f::Formula)
    if f.atoms[2] == 0
        view(f.atoms,3:3)
    elseif f.atoms[1] == 0
        view(f.atoms,2:3)
    else
        view(f.atoms, 1:3)
    end
end

#pretty-print lines in REPL and jupyter notebooks
function Base.show(io::IO, m::MIME"text/plain", f::Formula)
    print(io, *([atomic_symbols[i] for i in f.atoms if i != 0]...))
end

"""
    ismolecule(f::Formula)

`true` when `f` is composed of more than one atom
"""
ismolecule(f::Formula) = f.atoms[2] != 0

"""
    get_mass(f::Formula)

Returns the mass [g] of `f`.
"""
function get_mass(f::Formula)
    sum(atomic_masses[a] for a in get_atoms(f))
end

const roman_numerals = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X"]
"""
Represents an atom or molecule (a `Formula`) with a particular number of electrons (regardless of
their configuration).
"""
struct Species
    formula::Formula
    charge::Int
end

"""
    Species(code::AbstractString)

Parse the "species code" as it is often specified in linelists and return a the "astronomy"
notation. 01.00 → "H_I", 02.01 → "He_II", 02.1000 → "He_II", 0608 → "CO_I", etc.

To parse at compile time, use the `species` string macro, i.e. `species"H I"`.
"""
function Species(code::AbstractString)
    code = strip(code, ['0', ' '])
    toks = split(code, [' ', '.', '_'])
    if length(toks) > 2
        throw(ArgumentError(code * " isn't a valid species code"))
    end
    formula = Formula(toks[1])
    charge = if length(toks) == 1 || length(toks[2]) == 0
        0 #no charge specified -> assume neutral
    else
        charge = findfirst(toks[2] .== roman_numerals)
        charge = (charge isa Int ? charge : parse(Int, toks[2]))
        #if this is a MOOG-style numeric code, the charge is correct, otherwise subtract 1
        if tryparse(Float64, code) == nothing
            charge -= 1
        end
        charge
    end
    Species(formula, charge)
end

#used to contruct Species at compile time and avoid parsing in hot loops
macro species_str(s)
    Species(s)
end

#pretty-print lines in REPL and jupyter notebooks
function Base.show(io::IO, m::MIME"text/plain", s::Species)
    show(io, m, s.formula)
    print(io, " ", get_roman_numeral(s))
end

ismolecule(s::Species) = ismolecule(s.formula)
get_mass(s::Species) = get_mass(s.formula)
get_atoms(s::Species) = get_atoms(s.formula)
get_roman_numeral(s::Species) = roman_numerals[s.charge+1]

#This type represents an individual line.
struct Line{F}
    wl::F                     #cm
    log_gf::F                 #unitless
    species::Species
    E_lower::F                #eV (also called the excitation potential)
    gamma_rad::F              #s^-1
    gamma_stark::F            #s^-1
    vdW::Union{F, Tuple{F,F}} #either Γ_vdW [s^-1] per electron or (σ, α) from ABO theory

    @doc """
        Line(wl::F, log_gf::F, species::Species, E_lower::F,
             gamma_rad::Union{F, Missing}=missing, gamma_stark::Union{F, Missing}=missing,
             vdw::Union{F, Tuple{F, F}, Missing}, missing) where F <: Real

    Construct a `Line`.  If any of `gamma_rad`, `gamma_stark`, or `vdW` are `missing`, guess them.
    `vdW` may be log(Γ_vdW) (assumed if negative), Γ_vdW (assumed if 0 < `vdW` < 1), or packed ABO
    parameters (assumed if `vdW` > 1).  It may also be passed as a Tuple, `(σ, α)`.
    """
    function Line(wl::F, log_gf::F, species::Species, E_lower::F,
                  gamma_rad::Union{F, Missing}=missing, gamma_stark::Union{F, Missing}=missing,
                  vdW::Union{F, Tuple{F, F}, Missing}=missing) where F <: Real
        if ismissing(gamma_stark) || ismissing(vdW)
            gamma_stark_approx, vdW_approx = approximate_gammas(wl, species, E_lower)
            if ismissing(gamma_stark)
                gamma_stark = gamma_stark_approx
            end
            if ismissing(vdW)
                vdW = vdW_approx
            end
        end
        if ismissing(gamma_rad)
            gamma_rad = approximate_radiative_gamma(wl, log_gf)
        end

        if vdW isa F
            if vdW < 0 #if vdW is negative, assume it's log(Γ_vdW)
                vdW = 10^vdW
            elseif vdW > 1 #if it's > 1 assume it's packed ABO params
                vdW = (floor(vdW) * bohr_radius_cgs * bohr_radius_cgs, vdW - floor(vdW))
            end
        end

        new{F}(wl, log_gf, species, E_lower, gamma_rad, gamma_stark, vdW)
    end
end

#pretty-print lines in REPL and jupyter notebooks
function Base.show(io::IO, m::MIME"text/plain", line::Line)
    show(io, m, line.species)
    print(io, " ", round(line.wl*1e8, digits=6), " Å")
end

"""
    approximate_radiative_gamma(wl, log_gf)

Approximate radiate broadening parameter.
"""
function approximate_radiative_gamma(wl, log_gf)
    e = electron_charge_cgs
    m = electron_mass_cgs
    c = c_cgs
    8π^2 * e^2 / (m * c * wl^2) * 10^log_gf
end

"""
    approximate_gammas(wl, species, E_lower; ionization_energies=Korg.ionization_energies)

A simplified form of the Unsoeld (1955) approximation for van der Waals broadening and the
[Cowley 1971](https://ui.adsabs.harvard.edu/abs/1971Obs....91..139C/abstract) approximation for
Stark broadening, evaluated at 10,000 K.
Used for atomic lines with no vdW and stark broadening info in the linelist.

Returns `(γ_stark`, `log10(γ_vdW))` in Hz, where these are the per-perturber quantities.
For autoionizing lines (those for which E_upper > χ), Returns 0.0 for γ_vdW.

In the calculation of `n*²`, uses the approximation that
``\\overbar{r^2} = 5/2 {n^*}^4 / Z^2``
which neglects the dependence on the angular momentum quantum number, l, in the the form given by
[Warner 1967](https://ui.adsabs.harvard.edu/abs/1967MNRAS.136..381W/abstract) (the earliest english
work reporting the Unsoeld result).
"""
function approximate_gammas(wl, species, E_lower; ionization_energies=ionization_energies)
    Z = species.charge + 1 #Z is ionization stage, not atomic number
    if ismolecule(species) || Z > 3
        return 0.0,0.0
    end
    χ = ionization_energies[get_atoms(species.formula)[1]][Z]
    c = c_cgs
    h = hplanck_eV
    k = kboltz_cgs
    E_upper = E_lower + (h * c / wl)

    #It's not obvious to me which Rydberg constant to use here, and below in Δrbar2.  The sources
    #are not entirely clear. It doesn't make a big difference.
    nstar4_upper = (Z^2 * RydbergH_eV / (χ - E_upper))^2
    #I'm not actually able to reproduce Crowley 1971 equation 7 (his simplified form) from equation
    #5, but these match the values in the Turbospectrum source, so they are probably correct.
    #The constants here were calculated assuming that "v" is the mean (not modal) electron speed
    if Z == 1
        γstark = 2.25910152e-7 * nstar4_upper #Cowley (1971) equation 5 evaluated at T=10,000 K
    else
        #Cowley (1971) equation 6 @ T=10,000 K (n.b. the constant is 12/5 * that above)
        γstark = 5.42184365e-7 * nstar4_upper / (Z + 1)^2
    end

    Δrbar2 = (5/2) * Rydberg_eV^2 * Z^2 * (1/(χ - E_upper)^2 - 1/(χ - E_lower)^2)
    if χ < E_upper
        γvdW = 0.0
    else
        # (log) γ_vdW From R J Rutten's course notes.
        # Equations 11.29 and 11.30 from Gray 2005 are equivalent
        γvdW = 6.33 + 0.4log10(Δrbar2) + 0.3log10(10_000) + log10(k)
    end

    γstark, γvdW
end

"""
    read_linelist(filename; format="vald", isotopic_abundances=Korg.isotopic_abundances)

Parse a linelist file, returning a vector of [`Line`](@ref)s.

Pass `format="kurucz"` for a [Kurucz linelist](http://kurucz.harvard.edu/linelists.html)
(`format=kurucz_vac` if it uses vacuum wavelengths; Be warned that Korg will not assume that
wavelengths are vacuum below 2000 Å),`format="vald"` for a
[VALD](http://vald.astro.uu.se/~vald/php/vald.php) linelist, and `format="moog"` for a MOOG linelist
(doesn't support broadening parameters or dissociation energies).

VALD linelists (the default and preferred format) can be either "short" or "long" format,
"extract all" or "extract stellar".  Air wavelengths will automatically be converted into vacuum
wavelengths, and energy levels will be automatically converted from cm``^{-1}`` to eV.

When they are not pre-scaled by isotopic abundace (which VALD does by default), Korg will
automatically adjust the `log_gf` of each line according to `isotopic_abundances`, which defaults
to the values from [NIST](https://www.nist.gov/pml/atomic-weights-and-isotopic-compositions-relative-atomic-masses).
To use custom isotopic abundances, just pass `isotopic_abundances` as a dictionary mapping
`(atomic number, atomic weight)` pairs to abundances between 0 and 1.
"""
function read_linelist(fname::String; format="vald", isotopic_abundances::Dict=isotopic_abundances
                      ) :: Vector{Line}
    format = lowercase(format)
    linelist = open(fname) do f
        if format == "kurucz"
            parse_kurucz_linelist(f; vacuum=false)
        elseif format == "kurucz_vac"
            parse_kurucz_linelist(f; vacuum=true)
        elseif format == "vald"
            parse_vald_linelist(f, isotopic_abundances)
        elseif format == "moog"
            parse_moog_linelist(f)
        else
            throw(ArgumentError("$(format) is not a supported linelist format"))
        end
    end

    filter!(linelist) do line #filter triply+ ionized and hydrogen lines
        (0 <= line.species.charge <= 2) && (line.species != species"H_I")
    end

    #ensure linelist is sorted
    if !issorted(linelist, by=l->l.wl)
        sort!(linelist, by=l->l.wl)
    end

    linelist
end

#used to handle missing gammas in vald and kurucz lineslist parsers
expOrMissing(x) = x == 0.0 ? missing : 10.0^x
idOrMissing(x) = x == 0.0 ? missing : x

function parse_kurucz_linelist(f; vacuum=false)
    lines = Line[]
    for row in eachline(f)
        row == "" && continue #skip empty lines

        #some linelists have a missing column in the wavelenth region
        if length(row) == 159
            row = " " * row
        end

        #kurucz provides wavenumbers for "level 1" and "level 2", which is which is
        #determined by parity
        E_levels = map((row[25:36], row[53:64])) do s
            #abs because Kurucz multiplies predicted values by -1
            abs(parse(Float64,s)) * c_cgs * hplanck_eV
        end

        wl_transform = vacuum ? identity : air_to_vacuum

        push!(lines, Line(wl_transform(parse(Float64, row[1:11])*1e-7), #convert from nm to cm
                     parse(Float64, row[12:18]),
                     Species(row[19:24]),
                     min(E_levels...),
                     expOrMissing(parse(Float64, row[81:86])),
                     expOrMissing(parse(Float64, row[87:92])),
                     idOrMissing(parse(Float64, row[93:98]))))
    end
    lines
end

function parse_vald_linelist(f, isotopic_abundances)
    lines = filter!(collect(eachline(f))) do line
        length(line) > 0 && line[1] != '#' #remove comments and empty lines
    end

    lines = replace.(lines, "'"=>"\"") #replace single quotes with double

    # is this an "extract all" or an "extract stellar" linelist?
    extractall = !occursin(r"^\s+\d", lines[1])
    firstline = extractall ? 3 : 4
    header = lines[firstline - 1]

    scale_isotopes = any(startswith.(lines, "* oscillator strengths were NOT scaled "))
    if !scale_isotopes && !any(startswith.(lines,"* oscillator strengths were scaled "))
        throw(ArgumentError("Can't parse linelist.  Can't detect whether log(gf)s are scaled by "*
                            "isotopic abundance."))
    end

    #we take the linelist to be long-format when the second line after the header starts with a
    #space or a quote followed a space.  In some linelists the quotes are there, but in others
    #they are not.
    shortformat = !(occursin(r"^\"? ", lines[firstline + 1]))
    body = lines[firstline : (shortformat ? 1 : 4) : end]
    body = body[1 : findfirst(l->l[1]!='\"' || !isuppercase(l[2]), body)-1]

    CSVheader = if shortformat && extractall
        ["species", "wl", "E_low", "loggf", "gamma_rad", "gamma_stark", "gamma_vdW", "lande",
         "reference"]
    elseif shortformat #extract stellar
        ["species", "wl", "E_low", "Vmic", "loggf", "gamma_rad", "gamma_stark", "gamma_vdW",
         "lande", "depth", "reference"]
    else #long format (extract all or extract stellar)
        ["species", "wl", "loggf", "E_low", "J_lo", "E_up", "J_up", "lower_lande", "upper_lande",
         "mean_lande", "gamma_rad", "gamma_stark", "gamma_vdW"]
    end
    body = CSV.File(reduce(vcat, codeunits.(body.*"\n")), header=CSVheader, delim=',',
                    silencewarnings=true)

    E_low = if contains(header, "cm") #convert E_low to eV if necessary
        body.E_low * c_cgs * hplanck_eV
    elseif contains(header, "eV")
        body.E_low
    else
        throw(ArgumentError( "Can't parse linelist.  Can't determine energy units: " * E_col))
    end

    wl = if contains(header, "air") #convert wls to vacuum if necessary
        air_to_vacuum.(body.wl)
    elseif contains(header, "vac")
        body.wl
    else
        throw(ArgumentError( "Can't parse linelist.  Can't determine vac/air wls: " * header))
    end

    loggf = body.loggf .+ if scale_isotopes
        refs = if !shortformat #the references are on different lines
            lines[firstline+3 .+ ((0:length(body)-1) .* 4)]
        else #references are in the last column
            body.reference
        end

        map(refs) do ref
            #find things that look like (16)O or (64)Ni in reference string
            regexp = r"\((?<isotope>\d\d?\d?)\)(?<elem>\p{Lu}\p{Ll}?)"
            #add up the adjustments to log(gf) from isotopic abundances (zero if no info is present)
            log_probs = map(findall(regexp, ref)) do r
                m = match(regexp, ref[r])
                log10(isotopic_abundances[atomic_numbers[m["elem"]], parse(Int64, m["isotope"])])
            end
            sum([0 ; log_probs])
        end
    else
        0
    end

    Line.(wl * 1e-8, loggf, Species.(body.species), E_low, expOrMissing.(body.gamma_rad),
          expOrMissing.(body.gamma_stark), idOrMissing.(body.gamma_vdW))
end

#todo support moog linelists with broadening parameters?
function parse_moog_linelist(f)
    lines = collect(eachline(f))
    #moog format requires blank first line
    linelist = map(lines[2:end]) do line
        toks = split(line)
        Line(parse(Float64, toks[1]) * 1e-8, #convert Å to cm
             parse(Float64, toks[4]),
             Species(toks[2]),
             parse(Float64, toks[3]))
    end
    linelist
end


function IodLaplace(arg0::jdouble)
    return IodLaplace((jdouble,), arg0)
end

function estimate(obj::IodLaplace, arg0::Frame, arg1::PVCoordinates, arg2::AbsoluteDate, arg3::Vector3D, arg4::AbsoluteDate, arg5::Vector3D, arg6::AbsoluteDate, arg7::Vector3D)
    return jcall(obj, "estimate", CartesianOrbit, (Frame, PVCoordinates, AbsoluteDate, Vector3D, AbsoluteDate, Vector3D, AbsoluteDate, Vector3D), arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7)
end


include("step_script.jl")

# Simulate physics for 1/240 seconds (the default timestep)
function simstep(s::mjSim)
    # Create local simulator d (data), and m (model) variables
    d = s.d
    m = s.m

    if s.robot !== nothing
        # Check for gamepad input
        gamepad(s, GLFW.JOYSTICK_1)

        # Execute next step in command script
        step_script(s::mjSim, s.robot)

        # Step the controller forward by dt
        run!(s.robot)

        # Apply updated joint angles to sim
        d.ctrl .= unsafe_wrap(Array{Float64,1}, pointer(s.robot.state.joint_angles), 12)

        # If Pupper controller, subtract the l1 joint angles from the l2 joint angles
        # to fake the kinematics of the parallel linkage
        if true  # TODO: verify that the controller is a Pupper quadruped controller
            d.ctrl[[3,6,9,12]] .= d.ctrl[[3,6,9,12]] - d.ctrl[[2,5,8,11]]
        end
    end

    if s.paused
        if s.pert[].active > 0
            mjv_applyPerturbPose(m, d, s.pert, 1)  # move mocap and dynamic bodies
            mj_forward(m, d)
        end
    else
        #slow motion factor: 10x
        factor = s.slowmotion ? 10 : 1

        # advance effective simulation time by 1/refreshrate
        startsimtm = d.time
        starttm = time()
        refreshtm = 1.0/(factor*s.refreshrate)
        updates = refreshtm / m.opt.timestep

        steps = round(Int, round(s.framecount+updates)-s.framecount)
        s.framecount += updates

        for i=1:steps
            # clear old perturbations, apply new
            d.xfrc_applied .= 0.0
            if s.pert[].select > 0
                mjv_applyPerturbPose(m, d, s.pert, 0) # move mocap bodies only
                mjv_applyPerturbForce(m, d, s.pert)
            end

            mj_step(m, d)

            # break on reset
            (d.time < startsimtm) && break
        end
    end
end


#****************************************************************************
#                     Molecular Dynamics Potentials (MDP)
#                            CESMIX-MIT Project
#
#  Contributing authors: Ngoc-Cuong Nguyen (cuongng@mit.edu, exapde@gmail.com)
#****************************************************************************

function readconfig(app, mode)

filename = app.configfile;

if mode==0 # binary
    tmp = reinterpret(Float64,read(filename));
else # text
    tmp = Main.DelimitedFiles.readdlm(filename);
end
#tmp = tmp[:];

if mode==4
    config = readLAMMPS(tmp);
    config.nconfigs = length(config.natom); # number of configurations
    config.natomall = sum(config.natom);# number of atoms for all configurations
    return config;
end

if mode<=2
    nconfigs = int64(tmp[1]);          # number of configurations
    dim = int64(tmp[2]);
    ncq = int64(tmp[3]);
    ncv = int64(tmp[4]);
    nce = int64(tmp[5]);
    ncf = int64(tmp[6]);
    ncx = dim;

    app.nconfigs = nconfigs; # number of configurations
    app.dim = dim; # physical dimension
    app.ncx = dim; # number of compoments of x
    app.ncv = ncv; # number of compoments of v
    app.ncf = ncf; # number of compoments of f
    app.ncq = ncq; # number of compoments of q
    app.nce = nce; # number of compoments of e
    config = initializeconfig(app);
end

if mode<=1
    n2 = 6;
    m  = (1+nce+dim*dim)*nconfigs;
    n1 = n2+1;
    n2 = n2+m;
    tm = reshape(tmp[n1:n2], nconfigs, (1+nce+dim*dim));
    config.natom = tm[:,1]; # number of atoms per each configuration
    config.a = tm[:,2:(1+dim)]'; # 1st principal vectors per each configuration
    config.b = tm[:,(2+dim):(1+2*dim)]'; # 2nd principal vectors per each configuration
    if dim==3
        config.c = tm[:,(2+2*dim):(1+dim*dim)]'; # 2nd principal vectors per each configuration
    else
        config.c = [];
    end
    if nce>0
        config.e = tm[:,2+dim*dim]; # potential energy per each configuration
    end

    config.natomall = sum(config.natom);# number of atoms for all configurations
    m  = (1+ncx+ncq+ncv+ncf)*config.natomall;
    n1 = n2+1;
    n2 = n2+m;
    tm = reshape(tmp[n1:n2], config.natomall, (1+ncx+ncq+ncv+ncf));
    config.t = tm[:,1];
    config.x = tm[:,2:(1+ncx)]';
    config.q = tm[:,(2+ncx):(1+ncx+ncq)]';
    config.v = tm[:,(2+ncx+ncq):(1+ncx+ncq+ncv)]';
    config.f = tm[:,(2+ncx+ncq+ncv):(1+ncx+ncq+ncv+ncf)]';

elseif mode==2
    n2 = 6;
    m  = nconfigs;
    n1 = n2+1;
    n2 = n2+m;
    config.natom = reshape(tmp[n1:n2], 1, m); # number of atoms per each configuration
    config.natomall = sum(config.natom);# number of atoms for all configurations

    # the 1st principal vector of the simulation box
    m  = dim*nconfigs;
    n1 = n2+1;
    n2 = n2+m;
    config.a = reshape(tmp[n1:n2], dim, nconfigs); # the 1st principal vector of the simulation box

    # the 2nd principal vector of the simulation box
    n1 = n2+1;
    n2 = n2+m;
    config.b = reshape(tmp[n1:n2], dim, nconfigs); # the 2nd principal vector of the simulation box

    if dim==3
        # the 3rd principal vector of the simulation box
        n1 = n2+1;
        n2 = n2+m;
        config.c = reshape(tmp[n1:n2], dim, nconfigs); # the 3rd principal vector of the simulation box
    end

    m  = nconfigs;
    n1 = n2+1;
    n2 = n2+m;
    config.e = reshape(tmp[n1:n2], 1, m); # number of atoms per each configuration

    # read atom types for all configurations
    m  = config.natomall;
    n1 = n2+1;
    n2 = n2+m;
    config.t = reshape(int64(tmp[n1:n2]), 1, m);

    # read atom positions for all configurations
    m  = ncx*config.natomall;
    n1 = n2+1;
    n2 = n2+m;
    config.x = reshape(tmp[n1:n2], ncx, config.natomall);

    # read atom charges for all configurations
    m  = ncq*config.natomall;
    n1 = n2+1;
    n2 = n2+m;
    config.q = reshape(tmp[n1:n2], ncq, config.natomall);

    # read atom velocities for all configurations
    m  = ncv*config.natomall;
    n1 = n2+1;
    n2 = n2+m;
    config.v = reshape(tmp[n1:n2], ncv, config.natomall);

    # read atom forces for all configurations
    m  = ncf*config.natomall;
    n1 = n2+1;
    n2 = n2+m;
    config.f = reshape(tmp[n1:n2], ncf, config.natomall);
end

return config

end