"""
`module  JAC.Atomic`
    ... a submodel of JAC that contains all methods to set-up and process (simple) atomic and SCF computations as well as
        atomic representations.
"""
module Atomic

    ## using Interact
    using ..Basics, ..Radial, ..ManyElectron, ..Nuclear
    using ..Einstein, ..Hfs, ..IsotopeShift, ..PlasmaShift, ..LandeZeeman, ..AlphaVariation, ..FormFactor, ..DecayYield,
          ..MultipolePolarizibility, ..PhotoEmission, ..PhotoIonization, ..PhotoExcitation, ..PhotoRecombination,
          ..AutoIonization, ..DoubleAutoIonization, ..Dielectronic, ..ImpactExcitation, ..CoulombExcitation, ..CoulombIonization,
          ..PhotoDoubleIonization, ..PhotoExcitationFluores, ..PhotoExcitationAutoion, ..RayleighCompton, ..MultiPhotonDeExcitation,
          ..PhotoIonizationFluores, ..PhotoIonizationAutoion, ..ImpactExcitationAutoion, ..RadiativeAuger,
          ..MultiPhotonIonization, ..MultiPhotonDoubleIon, ..InternalConversion

    """
    `struct  Computation`
        ... defines a type for defining  (the model of simple) atomic computation of a single multiplet,
            including the SCF and CI as well as level properties and transition property calculations.

        + name                           ::String                          ... A name associated to the computation.
        + nuclearModel                   ::Nuclear.Model                   ... Model, charge and parameters of the nucleus.
        + grid                           ::Radial.Grid                     ... The radial grid to be used for the computation.
        + properties                     ::Array{AbstractLevelProperty,1}  ... List of atomic properties to be calculated.
        + configs                        ::Array{Configuration,1}          ... A list of non-relativistic configurations.
        + asfSettings                    ::AsfSettings
            ... Provides the settings for the SCF process and for the CI and QED calculations.
        + initialConfigs                 ::Array{Configuration,1}
            ... A list of initial-state configurations for some transition property calculation, such as radiative transition, Auger, etc.
        + initialAsfSettings             ::AsfSettings                     ... Provides the SCF and CI settings for the initial-state multiplet.
        + intermediateConfigs            ::Array{Configuration,1}          ... A list of initial-state configurations.
        + intermediateAsfSettings        ::AsfSettings                     ... Provides the SCF settings for the intermediate-state multiplet.
        + finalConfigs                   ::Array{Configuration,1}          ... A list of final-state configurations.
        + finalAsfSettings               ::AsfSettings                     ... Provides the SCF and CI settings for the final-state multiplet.
        + alphaSettings                  ::AlphaVariation.Settings         ... Settings for alpha-variation parameter calculations.
        + einsteinSettings               ::Einstein.Settings               ... Settings for Einstein coefficient calculations.
        + formSettings                   ::FormFactor.Settings             ... Settings for atomic form factor calculations.
        + hfsSettings                    ::Hfs.Settings                    ... Settings for hyperfine parameter calculations.
        + isotopeSettings                ::IsotopeShift.Settings           ... Settings for isotope shift parameter calculations.
        + plasmaSettings                 ::PlasmaShift.Settings            ... Settings for plasma-shift calculations.
        + polaritySettings               ::MultipolePolarizibility.Settings .. Settings for polarizibility calculations.
        + yieldSettings                  ::DecayYield.Settings             ... Settings for fluoresence and Auger yield calculations.
        + zeemanSettings                 ::LandeZeeman.Settings            ... Settings for Lande-Zeeman coefficient calculations.
        + process                        ::Basic.AbstractProcess
            ... An (additional) process for which the properties are to be evaluated for the given initial- and final-state configurations.
        + processSettings                ::Union{PhotoEmission.Settings, AutoIonization.Settings, PlasmaShift.AugerSettings,
                                                 PhotoIonization.Settings, PlasmaShift.PhotoSettings,
                                                 PhotoExcitation.Settings, PhotoExcitationAutoion.Settings, PhotoRecombination.Settings,
                                                 DoubleAutoIonization.Settings, PhotoDoubleIonization.Settings,
                                                 ImpactExcitation.Settings, Dielectronic.Settings, RadiativeAuger.Settings,
                                                 PairAnnihilation1Photon.Settings, ImpactExcitationAutoion.Settings,
                                                 MultiPhotonDeExcitation.Settings, CoulombExcitation.Settings,
                                                 CoulombIonization.Settings}
            ... Provides the settings for the selected process.
    """
    struct  Computation
        name                           ::String
        nuclearModel                   ::Nuclear.Model
        grid                           ::Radial.Grid
        properties                     ::Array{AbstractLevelProperty,1}
        configs                        ::Array{Configuration,1}
        asfSettings                    ::AsfSettings
        initialConfigs                 ::Array{Configuration,1}
        initialAsfSettings             ::AsfSettings
        intermediateConfigs            ::Array{Configuration,1}
        intermediateAsfSettings        ::AsfSettings
        finalConfigs                   ::Array{Configuration,1}
        finalAsfSettings               ::AsfSettings
        alphaSettings                  ::AlphaVariation.Settings
        einsteinSettings               ::Einstein.Settings
        formSettings                   ::FormFactor.Settings
        hfsSettings                    ::Hfs.Settings
        isotopeSettings                ::IsotopeShift.Settings
        plasmaSettings                 ::PlasmaShift.Settings
        polaritySettings               ::MultipolePolarizibility.Settings
        yieldSettings                  ::DecayYield.Settings
        zeemanSettings                 ::LandeZeeman.Settings
        process                        ::AbstractProcess
        processSettings                ::Union{PhotoEmission.Settings, AutoIonization.Settings, PlasmaShift.AugerSettings,
                                               PhotoIonization.Settings, PlasmaShift.PhotoSettings,
                                               PhotoRecombination.Settings, Dielectronic.Settings, ImpactExcitation.Settings,
                                               CoulombExcitation.Settings, CoulombIonization.Settings,
                                               PhotoExcitation.Settings, PhotoExcitationFluores.Settings,
                                               PhotoExcitationAutoion.Settings, RayleighCompton.Settings,
                                               DoubleAutoIonization.Settings, PhotoDoubleIonization.Settings,
                                               MultiPhotonDeExcitation.Settings, PhotoIonizationFluores.Settings,
                                               PhotoIonizationAutoion.Settings, ImpactExcitationAutoion.Settings,
                                               RadiativeAuger.Settings, MultiPhotonIonization.Settings,
                                               MultiPhotonDoubleIon.Settings, InternalConversion.Settings} #= ,
                                               #
                                               PairAnnihilation1Photon.Settings } =#
    end

    """
    `Atomic.Computation()`  ... constructor for an 'empty' instance::Atomic.Computation.
    """
    function Computation()
        Computation("", Nuclear.Model(1.), Radial.Grid(), AbstractLevelProperty[],
                    Configuration[], AsfSettings(),
                    Configuration[], AsfSettings(),
                    Configuration[], AsfSettings(),
                    Configuration[], AsfSettings(),
                    AlphaVariation.Settings(), Einstein.Settings(), FormFactor.Settings(), Hfs.Settings(), IsotopeShift.Settings(),
                    PlasmaShift.Settings(), MultipolePolarizibility.Settings(), DecayYield.Settings(),  LandeZeeman.Settings(),
                    Basics.NoProcess(), PhotoEmission.Settings() )
    end

    """
    `Atomic.Computation(comp::Atomic.Computation;`

        name=..,                nuclearModel=..,            grid=..,                    configs=..,                   asfSettings=..,
        initialConfigs=..,      initialAsfSettings=..,      intermediateConfigs=..,     intermediateAsfSettings=..,
        finalConfigs=..,        finalAsfSettings=..,        alphaSettings=..,           einsteinSettings=..,
        formSettings=..,        hfsSettings=..,             isotopeSettings=..,         plasmaSettings=..,
        polaritySettings=..,    yieldSettings::=..,         zeemanSettings=..,
        process=..,             processSettings=..,         printout::Bool=false)

        ... constructor for modifying the given Atomic.Computation by 'overwriting' the previously selected parameters.
    """
    function Computation(comp::Atomic.Computation;
        name::Union{Nothing,String}=nothing,                                        nuclearModel::Union{Nothing,Nuclear.Model}=nothing,
        grid::Union{Nothing,Radial.Grid}=nothing,                                   properties::Union{Nothing,Array{AbstractLevelProperty,1},Any}=nothing,
        configs::Union{Nothing,Array{Configuration,1}}=nothing,                     asfSettings::Union{Nothing,AsfSettings}=nothing,
        initialConfigs::Union{Nothing,Array{Configuration,1}}=nothing,              initialAsfSettings::Union{Nothing,AsfSettings}=nothing,
        intermediateConfigs::Union{Nothing,Array{Configuration,1}}=nothing,         intermediateAsfSettings::Union{Nothing,AsfSettings}=nothing,
        finalConfigs::Union{Nothing,Array{Configuration,1}}=nothing,                finalAsfSettings::Union{Nothing,AsfSettings}=nothing,
        alphaSettings::Union{Nothing,AlphaVariation.Settings}=nothing,              einsteinSettings::Union{Nothing,Einstein.Settings}=nothing,
        formSettings::Union{Nothing,FormFactor.Settings}=nothing,                   hfsSettings::Union{Nothing,Hfs.Settings}=nothing,
        isotopeSettings::Union{Nothing,IsotopeShift.Settings}=nothing,              plasmaSettings::Union{Nothing,PlasmaShift.Settings}=nothing,
        polaritySettings::Union{Nothing,MultipolePolarizibility.Settings}=nothing,  yieldSettings::Union{Nothing,DecayYield.Settings}=nothing,
        zeemanSettings::Union{Nothing,LandeZeeman.Settings}=nothing,
        process::Union{Nothing,Basics.AbstractProcess}=nothing,                     processSettings::Union{Nothing,Any}=nothing,
        printout::Bool=false)

        if  name                    == nothing  namex                    = comp.name                    else  namex                    = name                     end
        if  nuclearModel            == nothing  nuclearModelx            = comp.nuclearModel            else  nuclearModelx            = nuclearModel             end
        if  grid                    == nothing  gridx                    = comp.grid                    else  gridx                    = grid                     end
        if  properties              == nothing  propertiesx              = comp.properties              else  propertiesx              = properties               end
        if  configs                 == nothing  configsx                 = comp.configs                 else  configsx                 = configs                  end
        if  asfSettings             == nothing  asfSettingsx             = comp.asfSettings             else  asfSettingsx             = asfSettings              end
        if  initialConfigs          == nothing  initialConfigsx          = comp.initialConfigs          else  initialConfigsx          = initialConfigs           end
        if  initialAsfSettings      == nothing  initialAsfSettingsx      = comp.initialAsfSettings      else  initialAsfSettingsx      = initialAsfSettings       end
        if  intermediateConfigs     == nothing  intermediateConfigsx     = comp.intermediateConfigs     else  intermediateConfigsx     = intermediateConfigs      end
        if  intermediateAsfSettings == nothing  intermediateAsfSettingsx = comp.intermediateAsfSettings else  intermediateAsfSettingsx = intermediateAsfSettings  end
        if  finalConfigs            == nothing  finalConfigsx            = comp.finalConfigs            else  finalConfigsx            = finalConfigs             end
        if  finalAsfSettings        == nothing  finalAsfSettingsx        = comp.finalAsfSettings        else  finalAsfSettingsx        = finalAsfSettings         end
        if  alphaSettings           == nothing  alphaSettingsx           = comp.alphaSettings           else  alphaSettingsx           = alphaSettings            end
        if  einsteinSettings        == nothing  einsteinSettingsx        = comp.einsteinSettings        else  einsteinSettingsx        = einsteinSettings         end
        if  formSettings            == nothing  formSettingsx            = comp.formSettings            else  formSettingsx            = formSettings             end
        if  hfsSettings             == nothing  hfsSettingsx             = comp.hfsSettings             else  hfsSettingsx             = hfsSettings              end
        if  isotopeSettings         == nothing  isotopeSettingsx         = comp.isotopeSettings         else  isotopeSettingsx         = isotopeSettings          end
        if  plasmaSettings          == nothing  plasmaSettingsx          = comp.plasmaSettings          else  plasmaSettingsx          = plasmaSettings           end
        if  polaritySettings        == nothing  polaritySettingsx        = comp.polaritySettings        else  polaritySettingsx        = polaritySettings         end
        if  yieldSettings           == nothing  yieldSettingsx           = comp.yieldSettings           else  yieldSettingsx           = yieldSettings            end
        if  zeemanSettings          == nothing  zeemanSettingsx          = comp.zeemanSettings          else  zeemanSettingsx          = zeemanSettings           end
        if  process                 == nothing  processx                 = comp.process                 else  processx                 = process                  end
        if  processSettings         == nothing  prsx                     = comp.processSettings         else  prsx                     = processSettings          end

        if      processx == Basics.NoProcess                                                              prsx = PhotoEmission.Settings()
        elseif  processx == Basics.Auger            && typeof(prsx) != AutoIonization.Settings            prsx = AutoIonization.Settings()
        elseif  processx == Basics.AugerInPlasma    && typeof(prsx) != PlasmaShift.AugerSettings          prsx = PlasmaShift.AugerSettings()
        elseif  processx == Basics.Radiative        && typeof(prsx) != PhotoEmission.Settings             prsx = PhotoEmission.Settings()
        elseif  processx == Basics.PhotoExc         && typeof(prsx) != PhotoExcitation.Settings           prsx = PhotoExcitation.Settings()
        elseif  process == Basics.Photo             && typeof(prsx) != PhotoIonization.Settings           prsx = PhotoIonization.Settings()
        elseif  process == Basics.PhotoInPlasma     && typeof(prsx) != PlasmaShift.PhotoSettings          prsx = PlasmaShift.PhotoSettings()
        elseif  process == Basics.Rec               && typeof(prsx) != PhotoRecombination.Settings        prsx = PhotoRecombination.Settings()
        elseif  process == Basics.Dierec            && typeof(prsx) != Dielectronic.Settings              prsx = Dielectronic.Settings()
        elseif  process == Basics.PhotoExcFluor     && typeof(prsx) != PhotoExcitationFluores.Settings    prsx = PhotoExcitationFluores.Settings()
        elseif  process == Basics.PhotoExcAuto      && typeof(prsx) != PhotoExcitationAutoion.Settings    prsx = PhotoExcitationAutoion.Settings()
        elseif  process == Basics.PhotoIonFluor     && typeof(prsx) != PhotoIonizationFluores.Settings    prsx = PhotoIonizationFluores.Settings()
        elseif  process == Basics.PhotoIonAuto      && typeof(prsx) != PhotoIonizationAutoion.Settings    prsx = PhotoIonizationAutoion.Settings()
        elseif  process == Basics.MultiPhotonDE     && typeof(prsx) != MultiPhotonDeExcitation.Settings   prsx = MultiPhotonDeExcitation.Settings()
        elseif  process == Basics.Compton           && typeof(prsx) != RayleighCompton.Settings           prsx = RayleighCompton.Settings()
        elseif  process == Basics.Eimex             && typeof(prsx) != ImpactExcitation.Settings          prsx = ImpactExcitation.Settings()
        elseif  process == Basics.ImpactExcAuto     && typeof(prsx) != ImpactExcitationAutoion.Settings   prsx = ImpactExcitationAutoion.Settings()
        elseif  process == Basics.RAuger            && typeof(prsx) != RadiativeAuger.Settings            prsx = RadiativeAuger.Settings()
        elseif  process == Basics.MultiPI           && typeof(prsx) != MultiPhotonIonization.Settings     prsx = MultiPhotonIonization.Settings()
        elseif  process == Basics.MultiPDI          && typeof(prsx) != MultiPhotonDoubleIon.Settings      prsx = MultiPhotonDoubleIon.Settings()
        elseif  process == Basics.InternalConv      && typeof(prsx) != InternalConversion.Settings        prsx = InternalConversion.Settings()
        elseif  process == Basics.Coulex            && typeof(prsx) != CoulombExcitation.Settings         prsx = CoulombExcitation.Settings()
        elseif  process == Basics.Coulion           && typeof(prsx) != CoulombIonization.Settings         prsx = CoulombIonization.Settings()
        end

        cp = Computation(namex, nuclearModelx, gridx, propertiesx, configsx, asfSettingsx, initialConfigsx, initialAsfSettingsx,
                         intermediateConfigsx,  intermediateAsfSettingsx, finalConfigsx, finalAsfSettingsx,
                         alphaSettingsx, einsteinSettingsx , formSettingsx, hfsSettingsx, isotopeSettingsx, plasmaSettingsx,
                         polaritySettingsx, yieldSettingsx, zeemanSettingsx,  processx, prsx)

        if printout  Base.show(cp)      end
        return( cp )
    end

    """
    `Atomic.Computation( ... example for SCF computations)`

            grid     = Radial.Grid(true)
            nuclearM = Nuclear.Model(18., "Fermi")
            settings = AsfSettings(AsfSettings(), selectLevelsCI = true, selectedLevelsCI = [1,2, 4,5, 7,8], jjLS = LSjjSettings(false) )
            configs  = [Configuration("[Ne] 3s^2 3p^5"), Configuration("[Ne] 3s 3p^6")]
            Atomic.Computation(Atomic.Computation(), name="Example", grid=grid, nuclearModel=nuclearM, configs=configs, asfSettings=settings )

    `Atomic.Computation( ... example for the computation of atomic properties)`

            grid        = Radial.Grid(true)
            nuclearM    = Nuclear.Model(26., "Fermi", 58., 3.81, AngularJ64(5//2), 1.0, 1.0)
            hfsSettings = Hfs.Settings(true, true, false, false, false, false, false, Int64[] )
            configs     = [Configuration("[Ne] 3s"), Configuration("[Ne] 3p"), Configuration("[Ne] 3d")]
            Atomic.Computation(Atomic.Computation(), name="Example", grid=grid, nuclearModel=nuclearM, configs=configs, properties=[HFS()],
                               hfsSettings=hfsSettings )

    `Atomic.Computation( ... example for the computation of one atomic process)`

            grid           = Radial.Grid(true)
            initialConfigs = [Configuration("[Ne] 3s 3p^6"), Configuration("[Ne] 3s^2 3p^4 3d")]
            finalConfigs   = [Configuration("[Ne] 3s^2 3p^5")]
            photoSettings  = PhotoEmission.Settings(PhotoEmission.Settings(), multipoles=[E1, M1], gauges=[UseCoulomb], printBefore=true)
            Atomic.Computation(Atomic.Computation(), name="Example", grid=grid, nuclearModel=nuclearM;
                              initialConfigs=initialConfigs, finalConfigs=finalConfigs,
                              process = Radiative(), processSettings=photoSettings );

        ... These simple examples can be further improved by overwriting the corresponding parameters.
    """
    function Computation(wa::Bool)
        Atomic.Computation()
    end

    # `Base.string(comp::Atomic.Computation)`  ... provides a String notation for the variable comp::Atomic.Computation.
    function Base.string(comp::Atomic.Computation)
        sa = "Atomic computation:    $(comp.name) for Z = $(comp.nuclearModel.Z), "
        if  comp.process != NoProcess   sa = sa * "for the process $(comp.process) and with the \ninitial configurations:    "
            for  config  in  comp.initialConfigs   sa = sa * string(config) * ",  "         end
            if  length(comp.intermediateConfigs) > 0
                sa = sa * "\nintermediate configurations:"
                for  config  in  comp.finalConfigs     sa = sa * string(config) * ",  "     end
            end
            sa = sa * "\nfinal configurations:      "
            for  config  in  comp.finalConfigs     sa = sa * string(config) * ",  "         end
        else                                sa = sa * "for the properties $(comp.properties) and with the \nconfigurations:        "
            for  config  in  comp.configs   sa = sa * string(config) * ",  "                end
        end
        return( sa )
    end

    # `Base.show(io::IO, comp::Atomic.Computation)`  ... prepares a printout of comp::Atomic.Computation.
    function Base.show(io::IO, comp::Atomic.Computation)
        sa = Base.string(comp);             print(io, sa, "\n")
        println(io, "nuclearModel:          $(comp.nuclearModel)  ")
        println(io, "grid:                  $(comp.grid)  ")
        #
        # For the computation of some given atomic process
        if  comp.process != NoProcess
        println(io, "processSettings:              \n$(comp.processSettings)  ")
        if  comp.initialAsfSettings != AsfSettings()
        println(io, "initialAsfSettings:           \n$(comp.initialAsfSettings)  ")         end
        if  comp.intermediateAsfSettings != AsfSettings()
        println(io, "intermediateAsfSettings:      \n$(comp.intermediateAsfSettings)  ")    end
        if  comp.finalAsfSettings != AsfSettings()
        println(io, "finalAsfSettings:             \n$(comp.finalAsfSettings)  ")           end
        #
        else
        # For the computation of no or several atomic properties
        if  comp.asfSettings != AsfSettings()
        println(io, "asfSettings:                  \n$(comp.asfSettings)  ")                end
        if  AlphaX in comp.properties     &&  comp.alphaSettings    != AlphaVariation.Settings()
        println(io, "alphaSettings:                \n$(comp.alphaSettings)  ")              end
        if  EinsteinX in comp.properties  &&  comp.einsteinSettings != Einstein.Settings()
        println(io, "einsteinSettings:             \n$(comp.einsteinSettings)  ")           end
        if  FormF in comp.properties      &&  comp.formSettings     != FormFactor.Settings()
        println(io, "formSettings:                 \n$(comp.formSettings)  ")               end
        if  HFS in comp.properties        &&  comp.hfsSettings      != Hfs.Settings()
        println(io, "hfsSettings:                  \n$(comp.hfsSettings)  ")                end
        if  Isotope in comp.properties    &&  comp.isotopeSettings  != IsotopeShift.Settings()
        println(io, "isotopeSettings:              \n$(comp.isotopeSettings)  ")            end
        if  Plasma in comp.properties     &&  comp.plasmaSettings   != PlasmaShift.Settings()
        println(io, "plasmaSettings:               \n$(comp.plasmaSettings)  ")             end
        if  Polarity in comp.properties   &&  comp.polaritySettings != MultipolePolarizibility.Settings()
        println(io, "polaritySettings:             \n$(comp.polaritySettings)  ")           end
        if  Yields in comp.properties     &&  comp.yieldSettings    != DecayYield.Settings()
        println(io, "yieldSettings:                \n$(comp.yieldSettings)  ")              end
        if  Zeeman in comp.properties     &&  comp.zeemanSettings   != LandeZeeman.Settings()
        println(io, "zeemanSettings:               \n$(comp.zeemanSettings)  ")             end
        end
    end

end # module


#
#  Use git:                    git status ....;   git add <filenames>;   git commit -m "..";   git push;   git rm  <filenames>
#  Use Jupyter notebooks:      using IJulia;   notebook()
#  Activation:                 ];   pkg> up;   pkg> activate
#  Working with JAC:           using Revise;   using JAC;   include("../src/jac.jl");   pkg> test
#
"""
`module JAC`
    ... Jena Atomic Calculator (JAC) provides tools for performing atomic (structure) calculations at various degrees of complexity
        and sophistication. It has been designed to not only calculate atomic level structures and properties [such as g-factors or
        hyperfine and isotope-shift parameters] but also transition amplitudes between bound-state levels [for the anapole moment, dipole
        operator, electron electric-dipole moment, parity non-conservation, etc.] and, in particular, (atomic) transition probabilities,
        Auger rates, photoionization cross sections, radiative and dielectronic recombination rates as well as cross sections for many
        other (elementary) processes. In the future, JAC will also facilitate interactive computations, the simulation of atomic cascades,
        the time-evolution of statistical tensors as well as various semi-empirical estimates of atomic properties. --
        In addition, the JAC module supports the display of level energies, electron and photon spectra, radial orbitals and
        and other atomic data.

**`Perform (atomic) computations of different complexity:`**
    JAC will eventually support **eight kinds** of computations which can be summarized as follows:

   + Atomic computations, based on explicitly specified electron configurations.
   + Restricted active-space computations (RAS; not yet properly implemented).
   + Interactive computations.
   + Atomic cascade computations (not yet fully implemented).
   + Atomic responses (not yet implemented).
   + Time-evolution of statistical tensors in (intense) light pusles (not yet implemented).
   + Semi-empirical estimates of cross sections, etc. (not yet properly implemented).
   + Symbolic evaluation of expressions from Racah's algebra, etc. (not yet properly implemented).

**`Further details and information`**

        + Kinds of atomic implementation                                       [cf. ? Details.kindsOfComputation]
        + Atomic amplitudes (partly) implemented in JAC                        [cf. ? Details.amplitudes]
        + Atomic level properties (partly) implemented in JAC                  [cf. ? Details.properties]
        + Atomic processes (partly) implemented in JAC                         [cf. ? Details.processes]
        + Interactive use of JAC procedures                                    [cf. ? Details.interactive]
        + Design principles and limitations of the JAC program                 [cf. ? Details.design]
        + Data types, structs and name conventions of the JAC module           [cf. ? Details.datatypes]
        + Atomic cascade computations and approximations                       [cf. ? Details.decayCascades]
        + Use of (em) light pulses in the time evolution of statist. tensors   [cf. ? Details.pulses]
        + Why Julia ?                                                          [cf. ? Details.whyJulia]

"""
module JAC

using  Dates, Printf,  LinearAlgebra, Interact, SpecialFunctions, FortranFiles, GaussQuadrature, QuadGK, GSL, JLD

export @racahsum,
       add, analyze, AlphaVariation, AnapoleMoment, AngularJ64, AngularM64, AngularJ, AngularMomentum,
       AsfSettings, Atomic, Auger, AutoIonization,
       Basics, Basis,
       compute, convertUnits, Cascade, Configuration, ConfigurationR, Continuum, CsfR, CoulombExcitation, CoulombIonization,
       diagonalize, Defaults, DecayYield, Details, Dielectronic, DoubleAuger,
       estimate, ElectricDipoleMoment, Einstein, EmMultipole,
       E1, M1, E2, M2, E3, M3, E4, M4,
       FormFactor,
       generate, Green, GreenFunction, getDefaults, Gui,
       Hfs, HydrogenicIon,
       interpolate, integrate, ImpactExcitation, ImpactExcitationAutoion, ImpactIonization, InternalConversion, IsotopeShift,
       LandeZeeman, Level, LevelSymmetry, LSjj, LSjjSettings,
       ManyElectron, Model, modify, MultiPhotonDeExcitation, MultiPhotonDoubleIon, MultiPhotonIonization, MultipoleMoment,
       MultipolePolarizibility, Multiplet,
       NoAmplitude, NoProcess, Nuclear, NoneQed,
       Orbital,
       perform, provide, PairAnnihilation1Photon, PairAnnihilation2Photon, PairProduction, ParityNonConservation,
       PhotoEmission, PhotoExcitation, PhotoExcitationAutoion, PhotoExcitationFluores, PhotoIonization, PhotoIonizationFluores,
       PhotoIonizationAutoion, PhotoRecombination, PlasmaShift,
       QedPetersburg, QedSydney,
       Radial, RadialIntegrals, Radiative, RadiativeAuger, RayleighCompton, REDA, READI,
       SchiffMoment, Shell, Subshell, setDefaults,
       tabulate, tools,
       UseCoulomb, UseBabushkin, UseGauge

# Basic data and data structures
include("module-Basics.jl")
include("module-Radial.jl")
include("module-Math.jl")
include("module-Defaults.jl")
include("module-ManyElectron.jl")
include("module-Nuclear.jl")

# Specialized functions/methods to manipulate these data
include("module-AngularMomentum.jl")
include("module-AngularCoefficients-Ratip2013.jl")
include("module-Bsplines.jl")
include("module-Continuum.jl")
include("module-Details.jl")
include("module-RadialIntegrals.jl")
include("module-HydrogenicIon.jl")
include("module-InteractionStrength.jl")
include("module-InteractionStrengthQED.jl")
include("module-PeriodicTable.jl")
include("module-TableStrings.jl")
include("module-Tools.jl")
include("module-LSjj.jl")

# Functions/methods for atomic amplitudes
include("module-MultipoleMoment.jl")
include("module-ParityNonConservation.jl")

include("module-PhotoEmission.jl")

# Functions/methods for atomic properties
include("module-Einstein.jl")
include("module-Hfs.jl")
include("module-IsotopeShift.jl")
include("module-LandeZeeman.jl")
include("module-AlphaVariation.jl")
include("module-FormFactor.jl")
include("module-DecayYield.jl")
include("module-GreenFunction.jl")
include("module-MultipolePolarizibility.jl")
include("module-PlasmaShift.jl")

# Functions/methods for atomic processes
include("module-PhotoExcitation.jl")
include("module-PhotoIonization.jl")
include("module-PhotoRecombination.jl")
include("module-AutoIonization.jl")
include("module-Dielectronic.jl")
include("module-PhotoExcitationFluores.jl")
include("module-PhotoExcitationAutoion.jl")
include("module-RayleighCompton.jl")
include("module-MultiPhotonDeExcitation.jl")
include("module-CoulombExcitation.jl")
include("module-PhotoIonizationFluores.jl")
include("module-PhotoIonizationAutoion.jl")
include("module-CoulombIonization.jl")
include("module-ImpactExcitation.jl")
include("module-ImpactExcitationAutoion.jl")
include("module-RadiativeAuger.jl")
include("module-MultiPhotonIonization.jl")
include("module-MultiPhotonDoubleIon.jl")
include("module-InternalConversion.jl")      ## up to here + up to jac-tools
#= Further processes, not yet included into the code
include("module-DoubleAuger.jl")
include("module-REDA.jl")
include("module-READI.jl")
include("module-PairProduction.jl")
include("module-PairAnnihilation1Photon.jl")
include("module-PairAnnihilation2Photon.jl")  =#

# Functions/methods for atomic and cascade computations
include("module-Atomic.jl")
include("module-Cascade.jl")

# Functions/methods for atomic time evolutions
# include("module-Pulse.jl")
# include("module-Statistical.jl")

# Functions/methods for semi-empirical estimations
# include("module-ImpactIonization.jl")
# include("module-Semiempirical.jl")

# Functions/methods for symbolic computations
# include("module-Racah.jl")

# Basic functions/methods to manipulate these data
include("inc-module-Basics.jl")

# Specialized macros
include("macro-racahsum.jl")

# All test functions/methods stay with the JAC root module
include("jac-test.jl")

function __init__()
    # The following variables need to be initialized at runtime to enable precompilation
    global JAC_SUMMARY_IOSTREAM = stdout
    global JAC_TEST_IOSTREAM    = stdout
end

println("\nWelcome to JAC:  A fresh computational approach to atomic structures, processes, casacdes and time evolutions [(C) Copyright by Stephan Fritzsche, Jena (2019)].")

end


## core stream types ##

# the first argument to any IO MUST be a POINTER (to a JL_STREAM) or using show on it will cause memory corruption

# Generic IO functions

## byte-order mark, ntoh & hton ##

const ENDIAN_BOM = reinterpret(Uint32,uint8([1:4]))[1]

if ENDIAN_BOM == 0x01020304
    ntoh(x) = x
    hton(x) = x
    ltoh(x) = bswap(x)
    htol(x) = bswap(x)
elseif ENDIAN_BOM == 0x04030201
    ntoh(x) = bswap(x)
    hton(x) = bswap(x)
    ltoh(x) = x
    htol(x) = x
else
    error("seriously? what is this machine?")
end

isreadonly(s) = isreadable(s) && !iswritable(s)

## binary I/O ##

# all subtypes should implement this
write(s::IO, x::Uint8) = error(typeof(s)," does not support byte I/O")

write(io::IO, x) = throw(MethodError(write, (io, x)))
write(io::IO, xs...) = for x in xs write(io, x) end

if ENDIAN_BOM == 0x01020304
    function write(s::IO, x::Integer)
        sz = sizeof(x)
        for n = sz:-1:1
            write(s, uint8((x>>>((n-1)<<3))))
        end
        sz
    end
else
    function write(s::IO, x::Integer)
        sz = sizeof(x)
        for n = 1:sz
            write(s, uint8((x>>>((n-1)<<3))))
        end
        sz
    end
end

write(s::IO, x::Bool)    = write(s, uint8(x))
write(s::IO, x::Float16) = write(s, reinterpret(Int16,x))
write(s::IO, x::Float32) = write(s, reinterpret(Int32,x))
write(s::IO, x::Float64) = write(s, reinterpret(Int64,x))

function write(s::IO, a::AbstractArray)
    nb = 0
    for i = 1:length(a)
        nb += write(s, a[i])
    end
    nb
end

function write(s::IO, c::Char)
    if c < 0x80
        write(s, uint8(c))
        return 1
    elseif c < 0x800
        write(s, uint8(( c >> 6          ) | 0xC0))
        write(s, uint8(( c        & 0x3F ) | 0x80))
        return 2
    elseif c < 0x10000
        write(s, uint8(( c >> 12         ) | 0xE0))
        write(s, uint8(((c >> 6)  & 0x3F ) | 0x80))
        write(s, uint8(( c        & 0x3F ) | 0x80))
        return 3
    elseif c < 0x110000
        write(s, uint8(( c >> 18         ) | 0xF0))
        write(s, uint8(((c >> 12) & 0x3F ) | 0x80))
        write(s, uint8(((c >> 6)  & 0x3F ) | 0x80))
        write(s, uint8(( c        & 0x3F ) | 0x80))
        return 4
    else
        return write(s, '\ufffd')
    end
end

function write(s::IO, p::Ptr, n::Integer)
    for i=1:n
        write(s, unsafe_load(p, i))
    end
    n
end

# all subtypes should implement this
read(s::IO, x::Type{Uint8}) = error(typeof(s)," does not support byte I/O")

function read{T <: Integer}(s::IO, ::Type{T})
    x = zero(T)
    for n = 1:sizeof(x)
        x |= (convert(T,read(s,Uint8))<<((n-1)<<3))
    end
    return x
end

read(s::IO, ::Type{Bool})    = (read(s,Uint8)!=0)
read(s::IO, ::Type{Float16}) = box(Float16,unbox(Int16,read(s,Int16)))
read(s::IO, ::Type{Float32}) = box(Float32,unbox(Int32,read(s,Int32)))
read(s::IO, ::Type{Float64}) = box(Float64,unbox(Int64,read(s,Int64)))

read{T}(s::IO, t::Type{T}, d1::Int, dims::Int...) =
    read(s, t, tuple(d1,dims...))
read{T}(s::IO, t::Type{T}, d1::Integer, dims::Integer...) =
    read(s, t, map(int,tuple(d1,dims...)))

read{T}(s::IO, ::Type{T}, dims::Dims) = read!(s, Array(T, dims))

function read!{T}(s::IO, a::Array{T})
    for i = 1:length(a)
        a[i] = read(s, T)
    end
    return a
end

function read(s::IO, ::Type{Char})
    ch = read(s, Uint8)
    if ch < 0x80
        return char(ch)
    end

    # mimic utf8.next function
    trailing = Base.utf8_trailing[ch+1]
    c::Uint32 = 0
    for j = 1:trailing
        c += ch
        c <<= 6
        ch = read(s, Uint8)
    end
    c += ch
    c -= Base.utf8_offset[trailing+1]
    char(c)
end

function readuntil(s::IO, delim::Char)
    if delim < 0x80
        data = readuntil(s, uint8(delim))
        enc = byte_string_classify(data)
        return (enc==1) ? ASCIIString(data) : UTF8String(data)
    end
    out = IOBuffer()
    while !eof(s)
        c = read(s, Char)
        write(out, c)
        if c == delim
            break
        end
    end
    takebuf_string(out)
end

function readuntil{T}(s::IO, delim::T)
    out = T[]
    while !eof(s)
        c = read(s, T)
        push!(out, c)
        if c == delim
            break
        end
    end
    out
end

readline(s::IO) = readuntil(s, '\n')
readchomp(x) = chomp!(readall(x))

# read up to nb bytes into nb, returning # bytes read
function readbytes!(s::IO, b::AbstractArray{Uint8}, nb=length(b))
    olb = lb = length(b)
    nr = 0
    while nr < nb && !eof(s)
        a = read(s, Uint8)
        nr += 1
        if nr > lb
            lb = nr * 2
            resize!(b, lb)
        end
        b[nr] = a
    end
    if lb > olb
        resize!(b, nr) # shrink to just contain input data if was resized
    end
    return nr
end

# read up to nb bytes from s, returning a Vector{Uint8} of bytes read.
function readbytes(s::IO, nb=typemax(Int))
    b = Array(Uint8, min(nb, 65536))
    nr = readbytes!(s, b, nb)
    resize!(b, nr)
end

function readall(s::IO)
    b = readbytes(s)
    return is_valid_ascii(b) ? ASCIIString(b) : UTF8String(b)
end
readall(filename::String) = open(readall, filename)

## high-level iterator interfaces ##

type EachLine
    stream::IO
    ondone::Function
    EachLine(stream) = EachLine(stream, ()->nothing)
    EachLine(stream, ondone) = new(stream, ondone)
end
eachline(stream::IO) = EachLine(stream)

start(itr::EachLine) = nothing
function done(itr::EachLine, nada)
    if !eof(itr.stream)
        return false
    end
    itr.ondone()
    true
end
next(itr::EachLine, nada) = (readline(itr.stream), nothing)
eltype(itr::EachLine) = ByteString

readlines(s=STDIN) = collect(eachline(s))

## IOStream

const sizeof_ios_t = int(ccall(:jl_sizeof_ios_t, Int32, ()))

type IOStream <: IO
    handle::Ptr{Void}
    ios::Array{Uint8,1}
    name::String

    IOStream(name::String, buf::Array{Uint8,1}) = new(pointer(buf), buf, name)
end
# TODO: delay adding finalizer, e.g. for memio with a small buffer, or
# in the case where we takebuf it.
function IOStream(name::String, finalize::Bool)
    buf = zeros(Uint8,sizeof_ios_t)
    x = IOStream(name, buf)
    if finalize
        finalizer(x, close)
    end
    return x
end
IOStream(name::String) = IOStream(name, true)

convert(T::Type{Ptr{Void}}, s::IOStream) = convert(T, s.ios)
show(io::IO, s::IOStream) = print(io, "IOStream(", s.name, ")")
fd(s::IOStream) = int(ccall(:jl_ios_fd, Clong, (Ptr{Void},), s.ios))
close(s::IOStream) = ccall(:ios_close, Void, (Ptr{Void},), s.ios)
isopen(s::IOStream) = bool(ccall(:ios_isopen, Cint, (Ptr{Void},), s.ios))
function flush(s::IOStream)
    sigatomic_begin()
    systemerror("flush", ccall(:ios_flush, Cint, (Ptr{Void},), s.ios) != 0)
    sigatomic_end()
    s
end
iswritable(s::IOStream) = bool(ccall(:ios_get_writable, Cint, (Ptr{Void},), s.ios))
isreadable(s::IOStream) = bool(ccall(:ios_get_readable, Cint, (Ptr{Void},), s.ios))
modestr(s::IO) = modestr(isreadable(s), iswritable(s))
modestr(r::Bool, w::Bool) = r ? (w ? "r+" : "r") : (w ? "w" : error("Neither readable nor writable"))

function truncate(s::IOStream, n::Integer)
    systemerror("truncate", ccall(:ios_trunc, Int32, (Ptr{Void}, Uint), s.ios, n) != 0)
    return s
end

function seek(s::IOStream, n::Integer)
    ret = ccall(:ios_seek, FileOffset, (Ptr{Void}, FileOffset), s.ios, n)
    systemerror("seek", ret == -1)
    ret < -1 && error("seek failed")
    return s
end

seekstart(s::IO) = seek(s,0)

function seekend(s::IOStream)
    systemerror("seekend", ccall(:ios_seek_end, FileOffset, (Ptr{Void},), s.ios) != 0)
    return s
end

function skip(s::IOStream, delta::Integer)
    ret = ccall(:ios_skip, FileOffset, (Ptr{Void}, FileOffset), s.ios, delta)
    systemerror("skip", ret == -1)
    ret < -1 && error("skip failed")
    return s
end

function position(s::IOStream)
    pos = ccall(:ios_pos, FileOffset, (Ptr{Void},), s.ios)
    systemerror("position", pos == -1)
    return pos
end

eof(s::IOStream) = bool(ccall(:jl_ios_eof, Int32, (Ptr{Void},), s.ios))

# For interfacing with C FILE* functions

immutable CFILE
    ptr::Ptr{Void}
end

function CFILE(s::IO)
    @unix_only FILEp = ccall(:fdopen, Ptr{Void}, (Cint, Ptr{Uint8}), convert(Cint, fd(s)), modestr(s))
    @windows_only FILEp = ccall(:_fdopen, Ptr{Void}, (Cint, Ptr{Uint8}), convert(Cint, fd(s)), modestr(s))
    systemerror("fdopen", FILEp == C_NULL)
    seek(CFILE(FILEp), position(s))
end

convert(::Type{CFILE}, s::IO) = CFILE(s)

function seek(h::CFILE, offset::Integer)
    systemerror("fseek", ccall(:fseek, Cint, (Ptr{Void}, Clong, Cint),
                               h.ptr, convert(Clong, offset), int32(0)) != 0)
    h
end

position(h::CFILE) = ccall(:ftell, Clong, (Ptr{Void},), h.ptr)

## constructing and opening streams ##

# "own" means the descriptor will be closed with the IOStream
function fdio(name::String, fd::Integer, own::Bool=false)
    s = IOStream(name)
    ccall(:ios_fd, Ptr{Void}, (Ptr{Void}, Clong, Int32, Int32),
          s.ios, fd, 0, own);
    return s
end
fdio(fd::Integer, own::Bool=false) = fdio(string("<fd ",fd,">"), fd, own)

function open(fname::String, rd::Bool, wr::Bool, cr::Bool, tr::Bool, ff::Bool)
    s = IOStream(string("<file ",fname,">"))
    systemerror("opening file $fname",
                ccall(:ios_file, Ptr{Void},
                      (Ptr{Uint8}, Ptr{Uint8}, Int32, Int32, Int32, Int32),
                      s.ios, fname, rd, wr, cr, tr) == C_NULL)
    if ff
        systemerror("seeking to end of file $fname", ccall(:ios_seek_end, FileOffset, (Ptr{Void},), s.ios) != 0)
    end
    return s
end
open(fname::String) = open(fname, true, false, false, false, false)

function open(fname::String, mode::String)
    mode == "r"  ? open(fname, true , false, false, false, false) :
    mode == "r+" ? open(fname, true , true , false, false, false) :
    mode == "w"  ? open(fname, false, true , true , true , false) :
    mode == "w+" ? open(fname, true , true , true , true , false) :
    mode == "a"  ? open(fname, false, true , true , false, true ) :
    mode == "a+" ? open(fname, true , true , true , false, true ) :
    error("invalid open mode: ", mode)
end

function open(f::Function, args...)
    io = open(args...)
    try
        f(io)
    finally
        close(io)
    end
end

## low-level calls ##

write(s::IOStream, b::Uint8) = int(ccall(:ios_putc, Int32, (Uint8, Ptr{Void}), b, s.ios))

function write{T}(s::IOStream, a::Array{T})
    if isbits(T)
        if !iswritable(s)
            error("attempt to write to a read-only IOStream")
        end
        int(ccall(:ios_write, Uint, (Ptr{Void}, Ptr{Void}, Uint),
                  s.ios, a, length(a)*sizeof(T)))
    else
        invoke(write, (IO, Array), s, a)
    end
end

function write(s::IOStream, p::Ptr, nb::Integer)
    if !iswritable(s)
        error("attempt to write to a read-only IOStream")
    end
    int(ccall(:ios_write, Uint, (Ptr{Void}, Ptr{Void}, Uint), s.ios, p, nb))
end

function write{T,N,A<:Array}(s::IOStream, a::SubArray{T,N,A})
    if !isbits(T) || stride(a,1)!=1
        return invoke(write, (Any, AbstractArray), s, a)
    end
    colsz = size(a,1)*sizeof(T)
    if N<=1
        return write(s, pointer(a, 1), colsz)
    else
        cartesianmap((idxs...)->write(s, pointer(a, idxs), colsz),
                     tuple(1, size(a)[2:end]...))
        return colsz*trailingsize(a,2)
    end
end

# num bytes available without blocking
nb_available(s::IOStream) = ccall(:jl_nb_available, Int32, (Ptr{Void},), s.ios)

function read(s::IOStream, ::Type{Uint8})
    b = ccall(:ios_getc, Int32, (Ptr{Void},), s.ios)
    if b == -1
        throw(EOFError())
    end
    uint8(b)
end

function read!{T}(s::IOStream, a::Array{T})
    if isbits(T)
        nb = length(a)*sizeof(T)
        if ccall(:ios_readall, Uint,
                 (Ptr{Void}, Ptr{Void}, Uint), s.ios, a, nb) < nb
            throw(EOFError())
        end
    else
        invoke(read!, (IO, Array), s, a)
    end
    a
end

## text I/O ##

function write(s::IOStream, c::Char)
    if !iswritable(s)
        error("attempt to write to a read-only IOStream")
    end
    int(ccall(:ios_pututf8, Int32, (Ptr{Void}, Char), s.ios, c))
end
read(s::IOStream, ::Type{Char}) = ccall(:jl_getutf8, Char, (Ptr{Void},), s.ios)

takebuf_string(s::IOStream) =
    ccall(:jl_takebuf_string, ByteString, (Ptr{Void},), s.ios)

takebuf_array(s::IOStream) =
    ccall(:jl_takebuf_array, Vector{Uint8}, (Ptr{Void},), s.ios)

function takebuf_raw(s::IOStream)
    sz = position(s)
    buf = ccall(:jl_takebuf_raw, Ptr{Uint8}, (Ptr{Void},), s.ios)
    return buf, sz
end

function sprint(size::Integer, f::Function, args...)
    s = IOBuffer(Array(Uint8,size), true, true)
    truncate(s,0)
    f(s, args...)
    takebuf_string(s)
end

sprint(f::Function, args...) = sprint(0, f, args...)

write(x) = write(STDOUT::IO, x)

function readuntil(s::IOStream, delim::Uint8)
    ccall(:jl_readuntil, Array{Uint8,1}, (Ptr{Void}, Uint8), s.ios, delim)
end

function readbytes!(s::IOStream, b::Array{Uint8}, nb=length(b))
    olb = lb = length(b)
    nr = 0
    while !eof(s) && nr < nb
        if lb < nr+1
            lb = max(65536, (nr+1) * 2)
            resize!(b, lb)
        end
        nr += int(ccall(:ios_readall, Uint,
                        (Ptr{Void}, Ptr{Void}, Uint),
                        s.ios, pointer(b, nr+1), min(lb-nr, nb-nr)))
    end
    if lb > olb
        resize!(b, nr) # shrink to just contain input data if was resized
    end
    return nr
end

# based on code by Glen Hertz
function readuntil(s::IO, t::String)
    l = length(t)
    if l == 0
        return ""
    end
    if l > 40
        warn("readuntil(IO,String) will perform poorly with a long string")
    end
    out = IOBuffer()
    m = Array(Char, l)  # last part of stream to match
    t = collect(t)
    i = 0
    while !eof(s)
        i += 1
        c = read(s, Char)
        write(out, c)
        if i <= l
            m[i] = c
        else
            # shift to last part of s
            for j = 2:l
                m[j-1] = m[j]
            end
            m[l] = c
        end
        if i >= l && m == t
            break
        end
    end
    return takebuf_string(out)
end

## Character streams ##
const _chtmp = Array(Char, 1)
function peekchar(s::IOStream)
    if ccall(:ios_peekutf8, Int32, (Ptr{Void}, Ptr{Char}), s, _chtmp) < 0
        return char(-1)
    end
    return _chtmp[1]
end

function peek(s::IOStream)
    ccall(:ios_peekc, Int32, (Ptr{Void},), s)
end

function skipchars(s::IOStream, pred; linecomment::Char=char(0xffffffff))
    ch = peekchar(s); status = int(ch)
    while status >= 0 && (pred(ch) || ch == linecomment)
        if ch == linecomment
            readline(s)
        else
            read(s, Char)  # advance one character
        end
        ch = peekchar(s); status = int(ch)
    end
    return s
end

# BitArray I/O

write(s::IO, B::BitArray) = write(s, B.chunks)
read!(s::IO, B::BitArray) = read!(s, B.chunks)


__precompile__()

"""
# NoiseJul

A Julia module for seismological processing and analysis.
This module calls all submodules to perform seismological processing and analysis (data download, cross-correlation, noise monitoring etc.)
Each document can be found in their github repository.

- "IO" => "SeisIO",
- "Noise" => "SeisNoise",
- "Download" => "https://github.com/kura-okubo/SeisDownload",
- "Convert" => "https://github.com/xtyangpsp/SeisConvert.jl.git",
- #currently NOT available "Xcorrelation" => "https://github.com/jaredbryan881/SeisXcorrelation.jl",
- "RemoveEQ" => "https://github.com/kura-okubo/SeisRemoveEQ.jl",
- "Beamforming" => "https://github.com/kura-okubo/SeisBeamforming.jl.git")

## example:
S = NoiseJul.IO.SeisData()

Copyright (c) 2019 Harvard University Earthquake Seismology Group. All Rights Reserved.
"""
module NoiseJul

module IO
using SeisIO
for n in names(SeisIO, all=true)
    if Base.isidentifier(n) && n ∉ (Symbol(SeisIO), :eval)
        eval(Expr(:export, n))
    end
end
end

module Noise
using SeisNoise
for n in names(SeisNoise, all=true)
    if Base.isidentifier(n) && n ∉ (Symbol(SeisNoise), :eval)
        eval(Expr(:export, n))
    end
end
end

module Download
using SeisDownload
for n in names(SeisDownload, all=true)
    if Base.isidentifier(n) && n ∉ (Symbol(SeisDownload), :eval)
        eval(Expr(:export, n))
    end
end
end

module Convert
using SeisConvert
for n in names(SeisConvert, all=true)
    if Base.isidentifier(n) && n ∉ (Symbol(SeisConvert), :eval)
        eval(Expr(:export, n))
    end
end
end

module RemoveEQ
using SeisRemoveEQ
for n in names(SeisRemoveEQ, all=true)
    if Base.isidentifier(n) && n ∉ (Symbol(SeisRemoveEQ), :eval)
        eval(Expr(:export, n))
    end
end
end

module Beamforming
using SeisBeamforming
for n in names(SeisBeamforming, all=true)
    if Base.isidentifier(n) && n ∉ (Symbol(SeisBeamforming), :eval)
        eval(Expr(:export, n))
    end
end
end

end # module


#Subject
"""
    applylimitrule!(data::PKSubject, rule::LimitRule)

Apply rule to PK subject .

* STEP 1 (NaN step): replace all `NaN` and `missing` values with nan keyword value (if `nan` not NaN);
* STEP 2 (LLOQ step): replace values below `lloq` with `btmax` value if this value befor Tmax or with atmax if this value after Tmax (if `lloq` not NaN);
* STEP 3 (remove NaN): `rm` == true, then remove all `NaN` and `missing` values.
"""
function applylimitrule!(data::PKSubject, rule::LimitRule)
    applylimitrule!(data.time, data.obs, rule)
    data
end
"""
    applylimitrule!(f::Function, data::DataSet{T}, rule::LimitRule) where T <: PKSubject
"""
function applylimitrule!(f::Function, data::DataSet{T}, rule::LimitRule) where T <: PKSubject
    for i in data
        if f(i) applylimitrule!(i, rule) end
    end
    data
end
#DS ind Int
"""
    applylimitrule!(data::DataSet{T}, rule::LimitRule, ind::Int) where T <: PKSubject
"""
function applylimitrule!(data::DataSet{T}, rule::LimitRule, ind::Int) where T <: PKSubject
    applylimitrule!(data[ind], rule)
    data
end
#DS iter Int
"""
    applylimitrule!(data::DataSet{T}, rule::LimitRule, inds::Union{Vector{Int}, UnitRange{Int}, Tuple{Vararg{Int}}}) where T <: PKSubject
"""
function applylimitrule!(data::DataSet{T}, rule::LimitRule, inds::Union{Vector{Int}, UnitRange{Int}, Tuple{Vararg{Int}}}) where T <: PKSubject
    for i in inds
        applylimitrule!(data[i], rule)
    end
    data
end
#DS all
"""
    applylimitrule!(data::DataSet{T}, rule::LimitRule) where T <: PKSubject
"""
function applylimitrule!(data::DataSet{T}, rule::LimitRule) where T <: PKSubject
    for i = 1:length(data)
        applylimitrule!(data[i], rule)
    end
    data
end
#DS Dict
"""
    applylimitrule!(data::DataSet{T}, rule::LimitRule, sort::Dict) where T <: PKSubject
"""
function applylimitrule!(data::DataSet{T}, rule::LimitRule, sort::Dict) where T <: PKSubject
    for i = 1:length(data)
        if sort ⊆ data[i].id applylimitrule!(data[i], rule) end
    end
    data
end
"""
    applylimitrule!(time, obs, rule::LimitRule)
"""
function applylimitrule!(time, obs, rule::LimitRule)
    cmax, tmax, tmaxn = ctmax(time, obs)
    #NaN Rule
    obsn = length(obs)
    if !isnan(rule.nan)
        for i = 1:obsn
            if isnanormissing(obs[i])
                obs[i] = rule.nan
            end
        end
    end
    #LLOQ rule
    if !isnan(rule.lloq)
        for i = 1:obsn
            if !isnanormissing(obs[i]) && obs[i] <= rule.lloq
                if i <= tmaxn
                    obs[i] = rule.btmax
                else
                    obs[i] = rule.atmax
                end
            end
        end
    end
    #NaN Remove rule
    if rule.rm
        inds = findall(isnanormissing, obs)
        deleteat!(time, inds)
        deleteat!(obs, inds)
    end
    time, obs
end


struct DGModel{BL,G,NFND,NFD,GNF,AS,DS,D,MD}
  balancelaw::BL
  grid::G
  numfluxnondiff::NFND
  numfluxdiff::NFD
  gradnumflux::GNF
  auxstate::AS
  diffstate::DS
  direction::D
  modeldata::MD
end
function DGModel(balancelaw, grid, numfluxnondiff, numfluxdiff, gradnumflux;
                 auxstate=create_auxstate(balancelaw, grid),
                 diffstate=create_diffstate(balancelaw, grid),
                 direction=EveryDirection(), modeldata=nothing)
  DGModel(balancelaw, grid, numfluxnondiff, numfluxdiff, gradnumflux, auxstate,
          diffstate, direction, modeldata)
end

function (dg::DGModel)(dQdt, Q, ::Nothing, t; increment=false)
  bl = dg.balancelaw
  device = typeof(Q.data) <: Array ? CPU() : CUDA()

  grid = dg.grid
  topology = grid.topology

  dim = dimensionality(grid)
  N = polynomialorder(grid)
  Nq = N + 1
  Nqk = dim == 2 ? 1 : Nq
  Nfp = Nq * Nqk
  nrealelem = length(topology.realelems)

  Qvisc = dg.diffstate
  auxstate = dg.auxstate

  FT = eltype(Q)
  nviscstate = num_diffusive(bl, FT)

  lgl_weights_vec = grid.ω
  Dmat = grid.D
  vgeo = grid.vgeo
  sgeo = grid.sgeo
  vmapM = grid.vmapM
  vmapP = grid.vmapP
  elemtobndy = grid.elemtobndy
  polyorder = polynomialorder(dg.grid)

  Np = dofs_per_element(grid)

  communicate = !(isstacked(topology) &&
                  typeof(dg.direction) <: VerticalDirection)

  aux_comm = update_aux!(dg, bl, Q, t)
  @assert typeof(aux_comm) == Bool

  ########################
  # Gradient Computation #
  ########################
  if communicate
    MPIStateArrays.start_ghost_exchange!(Q)
    if aux_comm
      MPIStateArrays.start_ghost_exchange!(auxstate)
    end
  end

  if nviscstate > 0

    @launch(device, threads=(Nq, Nq, Nqk), blocks=nrealelem,
            volumeviscterms!(bl, Val(dim), Val(polyorder), dg.direction, Q.data,
                             Qvisc.data, auxstate.data, vgeo, t, Dmat,
                             topology.realelems))

    if communicate
      MPIStateArrays.finish_ghost_recv!(Q)
      if aux_comm
        MPIStateArrays.finish_ghost_recv!(auxstate)
      end
    end

    @launch(device, threads=Nfp, blocks=nrealelem,
            faceviscterms!(bl, Val(dim), Val(polyorder), dg.direction,
                           dg.gradnumflux, Q.data, Qvisc.data, auxstate.data,
                           vgeo, sgeo, t, vmapM, vmapP, elemtobndy,
                           topology.realelems))

    if communicate
      MPIStateArrays.start_ghost_exchange!(Qvisc)
    end

    aux_comm = update_aux_diffusive!(dg, bl, Q, t)
    @assert typeof(aux_comm) == Bool

    if aux_comm
      MPIStateArrays.start_ghost_exchange!(auxstate)
    end
  end

  ###################
  # RHS Computation #
  ###################
  @launch(device, threads=(Nq, Nq, Nqk), blocks=nrealelem,
          volumerhs!(bl, Val(dim), Val(polyorder), dg.direction, dQdt.data,
                     Q.data, Qvisc.data, auxstate.data, vgeo, t,
                     lgl_weights_vec, Dmat, topology.realelems, increment))

  if communicate
    if nviscstate > 0
      MPIStateArrays.finish_ghost_recv!(Qvisc)
      if aux_comm
        MPIStateArrays.finish_ghost_recv!(auxstate)
      end
    else
      MPIStateArrays.finish_ghost_recv!(Q)
      if aux_comm
        MPIStateArrays.finish_ghost_recv!(auxstate)
      end
    end
  end

  @launch(device, threads=Nfp, blocks=nrealelem,
          facerhs!(bl, Val(dim), Val(polyorder), dg.direction,
                   dg.numfluxnondiff,
                   dg.numfluxdiff,
                   dQdt.data, Q.data, Qvisc.data,
                   auxstate.data, vgeo, sgeo, t, vmapM, vmapP, elemtobndy,
                   topology.realelems))

  # Just to be safe, we wait on the sends we started.
  if communicate
    MPIStateArrays.finish_ghost_send!(Qvisc)
    MPIStateArrays.finish_ghost_send!(Q)
  end
end

function init_ode_state(dg::DGModel, args...;
                        forcecpu=false,
                        commtag=888)
  device = arraytype(dg.grid) <: Array ? CPU() : CUDA()

  bl = dg.balancelaw
  grid = dg.grid

  state = create_state(bl, grid, commtag)

  topology = grid.topology
  Np = dofs_per_element(grid)

  auxstate = dg.auxstate
  dim = dimensionality(grid)
  polyorder = polynomialorder(grid)
  vgeo = grid.vgeo
  nrealelem = length(topology.realelems)

  if !forcecpu
    @launch(device, threads=(Np,), blocks=nrealelem,
            initstate!(bl, Val(dim), Val(polyorder), state.data, auxstate.data, vgeo,
                     topology.realelems, args...))
  else
    h_vgeo = Array(vgeo)
    h_state = similar(state, Array)
    h_auxstate = similar(auxstate, Array)
    h_auxstate .= auxstate
    @launch(CPU(), threads=(Np,), blocks=nrealelem,
      initstate!(bl, Val(dim), Val(polyorder), h_state.data, h_auxstate.data, h_vgeo,
          topology.realelems, args...))
    state .= h_state
  end

  MPIStateArrays.start_ghost_exchange!(state)
  MPIStateArrays.finish_ghost_exchange!(state)

  return state
end

function indefinite_stack_integral!(dg::DGModel, m::BalanceLaw,
                                    Q::MPIStateArray, auxstate::MPIStateArray,
                                    t::Real)

  device = typeof(Q.data) <: Array ? CPU() : CUDA()

  grid = dg.grid
  topology = grid.topology

  dim = dimensionality(grid)
  N = polynomialorder(grid)
  Nq = N + 1
  Nqk = dim == 2 ? 1 : Nq

  FT = eltype(Q)

  vgeo = grid.vgeo
  polyorder = polynomialorder(dg.grid)

  # do integrals
  nintegrals = num_integrals(m, FT)
  nelem = length(topology.elems)
  nvertelem = topology.stacksize
  nhorzelem = div(nelem, nvertelem)

  @launch(device, threads=(Nq, Nqk, 1), blocks=nhorzelem,
          knl_indefinite_stack_integral!(m, Val(dim), Val(polyorder),
                                         Val(nvertelem), Q.data, auxstate.data,
                                         vgeo, grid.Imat, 1:nhorzelem,
                                         Val(nintegrals)))
end

# fallback
function update_aux!(dg::DGModel, bl::BalanceLaw, Q::MPIStateArray, t::Real)
  return false
end

function update_aux_diffusive!(dg::DGModel, bl::BalanceLaw, Q::MPIStateArray, t::Real)
  return false
end

function reverse_indefinite_stack_integral!(dg::DGModel, m::BalanceLaw,
                                            auxstate::MPIStateArray, t::Real)

  device = typeof(auxstate.data) <: Array ? CPU() : CUDA()

  grid = dg.grid
  topology = grid.topology

  dim = dimensionality(grid)
  N = polynomialorder(grid)
  Nq = N + 1
  Nqk = dim == 2 ? 1 : Nq

  FT = eltype(auxstate)

  vgeo = grid.vgeo
  polyorder = polynomialorder(dg.grid)

  # do integrals
  nintegrals = num_integrals(m, FT)
  nelem = length(topology.elems)
  nvertelem = topology.stacksize
  nhorzelem = div(nelem, nvertelem)

  @launch(device, threads=(Nq, Nqk, 1), blocks=nhorzelem,
          knl_reverse_indefinite_stack_integral!(Val(dim), Val(polyorder),
                                                 Val(nvertelem), auxstate.data,
                                                 1:nhorzelem,
                                                 Val(nintegrals)))
end

function nodal_update_aux!(f!, dg::DGModel, m::BalanceLaw, Q::MPIStateArray,
                           t::Real; diffusive=false)
  device = typeof(Q.data) <: Array ? CPU() : CUDA()

  grid = dg.grid
  topology = grid.topology

  dim = dimensionality(grid)
  N = polynomialorder(grid)
  Nq = N + 1
  nrealelem = length(topology.realelems)

  polyorder = polynomialorder(dg.grid)

  Np = dofs_per_element(grid)

  ### update aux variables
  if diffusive
    @launch(device, threads=(Np,), blocks=nrealelem,
            knl_nodal_update_aux!(m, Val(dim), Val(polyorder), f!,
                            Q.data, dg.auxstate.data, dg.diffstate.data, t,
                            topology.realelems))
  else
    @launch(device, threads=(Np,), blocks=nrealelem,
            knl_nodal_update_aux!(m, Val(dim), Val(polyorder), f!,
                            Q.data, dg.auxstate.data, t,
                            topology.realelems))
  end
end

function copy_stack_field_down!(dg::DGModel, m::BalanceLaw,
                                auxstate::MPIStateArray, fldin, fldout)

  device = typeof(auxstate.data) <: Array ? CPU() : CUDA()

  grid = dg.grid
  topology = grid.topology

  dim = dimensionality(grid)
  N = polynomialorder(grid)
  Nq = N + 1
  Nqk = dim == 2 ? 1 : Nq

  DFloat = eltype(auxstate)

  vgeo = grid.vgeo
  polyorder = polynomialorder(dg.grid)

  # do integrals
  nelem = length(topology.elems)
  nvertelem = topology.stacksize
  nhorzelem = div(nelem, nvertelem)

  @launch(device, threads=(Nq, Nqk, 1), blocks=nhorzelem,
          knl_copy_stack_field_down!(Val(dim), Val(polyorder), Val(nvertelem),
                                     auxstate.data, 1:nhorzelem, Val(fldin),
                                     Val(fldout)))
end

function MPIStateArrays.MPIStateArray(dg::DGModel, commtag=888)
  bl = dg.balancelaw
  grid = dg.grid

  state = create_state(bl, grid, commtag)

  return state
end


using CSyntax
using GLTF

@static if Sys.isapple()
    const VERSION_MAJOR = 4
    const VERSION_MINOR = 1
end

include(joinpath(@__DIR__, "glutils.jl"))
include(joinpath(@__DIR__, "camera.jl"))

# init window
width, height = 640, 480
window = startgl(width, height)

glEnable(GL_DEPTH_TEST)
glDepthFunc(GL_LESS)

# we use glTF file format because it's OpenGL friendly and we don't need to care about
# how to parse and extract mesh data from other mesh file format like .obj format.
# load glTF file
suzanne = GLTF.load(joinpath(@__DIR__, "monkey2.gltf"))
suzanne_data = [read(joinpath(@__DIR__, b.uri)) for b in suzanne.buffers]

# load suzanne position metadata
search_name(x, keyword) = x[findfirst(x->occursin(keyword, x.name), x)]
pos = search_name(suzanne.accessors, "positions")
pos_bv = suzanne.bufferViews[pos.bufferView]
# load suzanne index metadata
indices = search_name(suzanne.accessors, "indices")
idx_bv = suzanne.bufferViews[indices.bufferView]
# load suzanne texture coordinate metadata
texcoords = search_name(suzanne.accessors, "texcoords")
tex_bv = suzanne.bufferViews[texcoords.bufferView]
# load suzanne normal metadata
normals = search_name(suzanne.accessors, "normals")
normal_bv = suzanne.bufferViews[normals.bufferView]

# create VBO and EBO
pos_vbo = GLuint(0)
@c glGenBuffers(1, &pos_vbo)
glBindBuffer(pos_bv.target, pos_vbo)
glBufferData(pos_bv.target, pos_bv.byteLength, C_NULL, GL_STATIC_DRAW)
pos_data = suzanne_data[pos_bv.buffer]
@c glBufferSubData(pos_bv.target, 0, pos_bv.byteLength, &pos_data[pos_bv.byteOffset])

normal_vbo = GLuint(0)
@c glGenBuffers(1, &normal_vbo)
glBindBuffer(normal_bv.target, normal_vbo)
glBufferData(normal_bv.target, normal_bv.byteLength, C_NULL, GL_STATIC_DRAW)
normal_data = suzanne_data[normal_bv.buffer]
@c glBufferSubData(normal_bv.target, 0, normal_bv.byteLength, &normal_data[normal_bv.byteOffset])

tex_vbo = GLuint(0)
@c glGenBuffers(1, &tex_vbo)
glBindBuffer(tex_bv.target, tex_vbo)
glBufferData(tex_bv.target, tex_bv.byteLength, C_NULL, GL_STATIC_DRAW)
tex_data = suzanne_data[tex_bv.buffer]
@c glBufferSubData(tex_bv.target, 0, tex_bv.byteLength, &tex_data[tex_bv.byteOffset])

idx_ebo = GLuint(0)
@c glGenBuffers(1, &idx_ebo)
glBindBuffer(idx_bv.target, idx_ebo)
glBufferData(idx_bv.target, idx_bv.byteLength, C_NULL, GL_STATIC_DRAW)
idx_data = suzanne_data[idx_bv.buffer]
@c glBufferSubData(idx_bv.target, 0, idx_bv.byteLength, &idx_data[idx_bv.byteOffset])

# create VAO
vao = GLuint(0)
@c glGenVertexArrays(1, &vao)
glBindVertexArray(vao)
# bind position vbo
glBindBuffer(pos_bv.target, pos_vbo)
glVertexAttribPointer(0, 3, pos.componentType, pos.normalized, pos_bv.byteStride, Ptr{Cvoid}(pos.byteOffset))
glEnableVertexAttribArray(0)
# bind normal vbo
glBindBuffer(normal_bv.target, normal_vbo)
glVertexAttribPointer(1, 3, normals.componentType, normals.normalized, normal_bv.byteStride, Ptr{Cvoid}(normals.byteOffset))
glEnableVertexAttribArray(1)
# bind texture coordinate vbo
glBindBuffer(tex_bv.target, tex_vbo)
glVertexAttribPointer(2, 2, texcoords.componentType, texcoords.normalized, tex_bv.byteStride, Ptr{Cvoid}(texcoords.byteOffset))
glEnableVertexAttribArray(2)

# camera
camera = PerspectiveCamera()
setposition!(camera, [0.0, 0.0, 5.0])

# create shader program
vert_shader = createshader(joinpath(@__DIR__, "mesh.vert"), GL_VERTEX_SHADER)
frag_shader = createshader(joinpath(@__DIR__, "mesh.frag"), GL_FRAGMENT_SHADER)

# link program
shader_prog = createprogram(vert_shader, frag_shader)
view_loc = glGetUniformLocation(shader_prog, "view")
proj_loc = glGetUniformLocation(shader_prog, "proj")
glUseProgram(shader_prog)
glUniformMatrix4fv(view_loc, 1, GL_FALSE, get_view_matrix(camera))
glUniformMatrix4fv(proj_loc, 1, GL_FALSE, get_projective_matrix(window, camera))

# # enable cull face
glEnable(GL_CULL_FACE)
glCullFace(GL_BACK)
glFrontFace(GL_CCW)
# set background color to gray
glClearColor(0.2, 0.2, 0.2, 1.0)

let
updatefps = FPSCounter()
# render
while !GLFW.WindowShouldClose(window)
    updatefps(window)
    # clear drawing surface
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glViewport(0, 0, GLFW.GetFramebufferSize(window)...)
    # drawing
    glUseProgram(shader_prog)
    glBindVertexArray(vao)
    glBindBuffer(idx_bv.target, idx_ebo)
    glDrawElements(GL_TRIANGLES, indices.count, indices.componentType, Ptr{Cvoid}(0))
    # check and call events
    GLFW.PollEvents()
    # move camera
    updatecamera!(window, camera)
    glUniformMatrix4fv(view_loc, 1, GL_FALSE, get_view_matrix(camera))
    glUniformMatrix4fv(proj_loc, 1, GL_FALSE, get_projective_matrix(window, camera))
    # swap the buffers
    GLFW.SwapBuffers(window)
end
end # let

GLFW.DestroyWindow(window)


using ImageDraw, Plots

function draw_boxes_and_landmarks(img, boxes, lms; conf=nothing, len=640)
    to_draw = deepcopy(RGB{N0f8}.(img[1,:,:], img[2,:,:], img[3,:,:]))
    colors = [
        RGB{N0f8}(1.0,0.0,0.0), # red
        RGB{N0f8}(0.0,0.0,1.0), # blue
        RGB{N0f8}(0.0,1.0,0.0), # green
        RGB{N0f8}(1.0,1.0,0.0), # yellow
        RGB{N0f8}(0.0,1.0,1.0), # turquoise
        RGB{N0f8}(1.0,0.0,1.0) # magenta
    ]

    for person in 1:size(boxes, 2)
        rect = Int.(ceil.(boxes[:, person]))
        if any(x->x >= 0, boxes[1:4, person])
            to_draw = draw!(to_draw, Polygon([
                        (rect[1, 1], rect[2, 1]),
                        (rect[3, 1], rect[2, 1]),
                        (rect[3, 1], rect[4, 1]),
                        (rect[1, 1], rect[4, 1])]), colors[1])
        end
        colorid = 2
        if lms !== nothing
            pts = Int.(ceil.(lms[:, person]))
            for lm in 1:2:10
                if pts[lm,1] >= 0 && pts[lm+1,1] >= 0
                   to_draw = draw!(to_draw, CirclePointRadius(pts[lm,1],pts[lm+1,1],2), colors[colorid])
                end
                colorid += 1
            end
        end
    end

    if conf !== nothing
        plt = plot(to_draw, size=(len,len))
        for person in 1:size(conf)[2]
            pt_x = boxes[1, person] + (boxes[3, person] - boxes[1, person]) / 2
            pt_y = boxes[2, person] - 10
            annotate!(pt_x, pt_y, text(string(round.(value(conf[2,person]); digits=4)), :orange, 6))
        end
        return plt
    else
        return to_draw
    end

end


module SingleCrystal

using LinearAlgebra
using JSON

# https://web.archive.org/web/20080324193801/http://cst-www.nrl.navy.mil/lattice/spcgrp/
# coordinate system -> standard cartesian
# primitive vectors -> describe the primitive cell
# basis vectors -> describe positions of atoms in the primitive cell
# translation of basis vectors by primitive vectors generate a crystal

# https://wiki.fysik.dtu.dk/ase/ase/spacegroup/spacegroup.html

"""
    get_chemical_info()

Returns instances `chemical_symbols`, `atomic_numbers` and `masses` for the setup of `Cell`.
"""
function get_chemical_info()
    chemical_symbols = [
        # 0
        "X",
        # 1
        "X", "He",
        # 2
        "Li", "Be", "B", "C", "N", "O", "F", "Ne",
        # 3
        "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar",
        # 4
        "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn",
        "Ga", "Ge", "As", "Se", "Br", "Kr",
        # 5
        "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd",
        "In", "Sn", "Sb", "Te", "I", "Xe",
        # 6
        "Cs", "Ba", "La", "Ce", "Pr", "Nd", "Pm", "Sm", "Eu", "Gd", "Tb", "Dy",
        "Ho", "Er", "Tm", "Yb", "Lu",
        "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg", "Tl", "Pb", "Bi",
        "Po", "At", "Rn",
        # 7
        "Fr", "Ra", "Ac", "Th", "Pa", "U", "Np", "Pu", "Am", "Cm", "Bk",
        "Cf", "Es", "Fm", "Md", "No", "Lr",
        "Rf", "Db", "Sg", "Bh", "Hs", "Mt", "Ds", "Rg", "Cn", "Nh", "Fl", "Mc",
        "Lv", "Ts", "Og"
    ]

    atomic_numbers = Dict()
    for (Z, symbol) in enumerate(chemical_symbols)
        atomic_numbers[symbol] = Z
    end

    atomic_masses_iupac2016 = [
        1.0,  # X
        1.008,  # H [1.00784, 1.00811]
        4.002602,  # He
        6.94,  # Li [6.938, 6.997]
        9.0121831,  # Be
        10.81,  # B [10.806, 10.821]
        12.011,  # C [12.0096, 12.0116]
        14.007,  # N [14.00643, 14.00728]
        15.999,  # O [15.99903, 15.99977]
        18.998403163,  # F
        20.1797,  # Ne
        22.98976928,  # Na
        24.305,  # Mg [24.304, 24.307]
        26.9815385,  # Al
        28.085,  # Si [28.084, 28.086]
        30.973761998,  # P
        32.06,  # S [32.059, 32.076]
        35.45,  # Cl [35.446, 35.457]
        39.948,  # Ar
        39.0983,  # K
        40.078,  # Ca
        44.955908,  # Sc
        47.867,  # Ti
        50.9415,  # V
        51.9961,  # Cr
        54.938044,  # Mn
        55.845,  # Fe
        58.933194,  # Co
        58.6934,  # Ni
        63.546,  # Cu
        65.38,  # Zn
        69.723,  # Ga
        72.630,  # Ge
        74.921595,  # As
        78.971,  # Se
        79.904,  # Br [79.901, 79.907]
        83.798,  # Kr
        85.4678,  # Rb
        87.62,  # Sr
        88.90584,  # Y
        91.224,  # Zr
        92.90637,  # Nb
        95.95,  # Mo
        97.90721,  # 98Tc
        101.07,  # Ru
        102.90550,  # Rh
        106.42,  # Pd
        107.8682,  # Ag
        112.414,  # Cd
        114.818,  # In
        118.710,  # Sn
        121.760,  # Sb
        127.60,  # Te
        126.90447,  # I
        131.293,  # Xe
        132.90545196,  # Cs
        137.327,  # Ba
        138.90547,  # La
        140.116,  # Ce
        140.90766,  # Pr
        144.242,  # Nd
        144.91276,  # 145Pm
        150.36,  # Sm
        151.964,  # Eu
        157.25,  # Gd
        158.92535,  # Tb
        162.500,  # Dy
        164.93033,  # Ho
        167.259,  # Er
        168.93422,  # Tm
        173.054,  # Yb
        174.9668,  # Lu
        178.49,  # Hf
        180.94788,  # Ta
        183.84,  # W
        186.207,  # Re
        190.23,  # Os
        192.217,  # Ir
        195.084,  # Pt
        196.966569,  # Au
        200.592,  # Hg
        204.38,  # Tl [204.382, 204.385]
        207.2,  # Pb
        208.98040,  # Bi
        208.98243,  # 209Po
        209.98715,  # 210At
        222.01758,  # 222Rn
        223.01974,  # 223Fr
        226.02541,  # 226Ra
        227.02775,  # 227Ac
        232.0377,  # Th
        231.03588,  # Pa
        238.02891,  # U
        237.04817,  # 237Np
        244.06421,  # 244Pu
        243.06138,  # 243Am
        247.07035,  # 247Cm
        247.07031,  # 247Bk
        251.07959,  # 251Cf
        252.0830,  # 252Es
        257.09511,  # 257Fm
        258.09843,  # 258Md
        259.1010,  # 259No
        262.110,  # 262Lr
        267.122,  # 267Rf
        268.126,  # 268Db
        271.134,  # 271Sg
        270.133,  # 270Bh
        269.1338,  # 269Hs
        278.156,  # 278Mt
        281.165,  # 281Ds
        281.166,  # 281Rg
        285.177,  # 285Cn
        286.182,  # 286Nh
        289.190,  # 289Fl
        289.194,  # 289Mc
        293.204,  # 293Lv
        293.208,  # 293Ts
        294.214,  # 294Og
    ]

    masses = Dict(k => atomic_masses_iupac2016[atomic_numbers[k]] for k in chemical_symbols)

    return chemical_symbols, atomic_numbers, masses
end

function load_spgs(;file::String="$(@__DIR__)/spacegroup.json")
    return JSON.parsefile(file)
end

function load_refs(;file::String="$(@__DIR__)/ase-atoms.json")
    return JSON.parsefile(file)
end

"""
    parse_json_spacegroup(spgs,nr,setting)

Formats the info of a specific spacegroup, preparing its use in `get_symops`.
"""
function parse_json_spacegroup(spgs::Dict,nr::T0,setting::T1) where {T0<:Integer,T1<:Integer}
    spg = spgs["$(nr): $(setting)"]
    spg["subtrans"] = [Array{Float64}(reshape(v,3)) for v in spg["subtrans"]]
    spg["translations"] = [Array{Float64}(reshape(v,3)) for v in spg["translations"]]
    spg["rotations"] = [Array{Float64}(hcat(v...)) for v in spg["rotations"]]
    return spg
end

"""
    get_symops(spg)

Generates rotation and translation operations for the given spacegroup info (`spg` from `parse_json_spacegroup`).
"""
function get_symops(spg::Dict)
    #  `spg` is the output of `parse_json_spacegroup`
    parities = spg["centrosymmetric"] ? [1,-1] : [1]
    symops = []
    @assert length(spg["rotations"]) == length(spg["translations"])
    for (parity, trans_sub) in Iterators.product(parities, spg["subtrans"])
        for (rot, trans) in Iterators.zip(spg["rotations"], spg["translations"])
            push!(symops, (parity * rot,
                           (trans + trans_sub) .% 1))
        end
    end
    return symops
end

"""
    fold(x)

Folding `x` back into [0,1).
"""
function fold(x::T) where T<:Any
    return x < 0 ? x + 1 : x
end

"""
    get_equivalent_sites(basis, symops)

Computes equivalent sites of in `basis` using rotations and translations in `symops`.
"""
function get_equivalent_sites(basis::Array{Array{Float64,2},1}, symops)
    kinds, sites = [], []

    for (kind, pos) in enumerate(basis)
        for (rot, trans) in symops
            site = (transpose(pos * rot) + trans) .% 1
            site = fold.(site)
            isdifferent = !any([v ≈ site for v in sites])
            if ((length(sites) == 0) | isdifferent)
                push!(sites, site)
                append!(kinds, kind)
            end
        end
    end
    return sites, kinds
end

"""
    make_unit_vec(x)

Computes the normalized `x` vector.
"""
function make_unit_vec(x::Array{T,1}) where T<:Any
    return x / norm(x)
end

"""
    get_coords(a_direction, ab_normal)

Computes the coordinate system given `a_direction` (usually [1 0 0]) and the vector normal to `a` and `b` (`ab_normal`, usually [0 0 1]).
"""
function get_coords(a_direction::Array{Float64,1}, ab_normal::Array{Float64,1})
    @assert dot(a_direction, ab_normal) ≈ 0.
    _x = make_unit_vec(a_direction)
    z = make_unit_vec(ab_normal)

    x = _x - dot(_x, ab_normal) * z
    xyz = hcat(x, cross(z,x), z)
    return xyz
end

"""
    deg2rad(x)

Convert degrees to radians.
"""
function deg2rad(x::T) where T <: Real
    return x * π / 180.
end

"""
    get_cos(x)

Compute the cosine of x degrees.
"""
function get_cos(x::T) where T <: Real
    return x ≈ 90 ? 0 : cos(deg2rad(x))
end

"""
    get_cell_vectors(cellpar)

Computes 3x3 cell vectors (column-wise). cellpar = [a, b, c, α, β, γ]
"""
function get_cell_vectors(cellpar::Array{Float64,1})
    a, b, c, α, β, γ = cellpar
    cos_α = get_cos(α)
    cos_β = get_cos(β)
    cos_γ = get_cos(abs(γ))
    sin_γ = abs(γ) ≈ 90 ? sign(γ) : sin(deg2rad(γ))
    cos_α, cos_β, cos_γ, sin_γ

    cy = (cos_α - cos_β * cos_γ) / sin_γ
    abc = hcat([a; 0; 0], b*[cos_γ; sin_γ; 0], c*[cos_β; cy; √(1-cos_β*cos_β-cy*cy)])
    return abc
end

struct CoordinateSystem{T}
    x::Array{T}
    y::Array{T}
    z::Array{T}
    M::Matrix{T} # contains x,y,z as column vectors
end

"""
    CartesianCoords(T)

Initialization of the cartesian coordiantes using `CoordianteSystem`.
"""
CartesianCoords(T) = CoordinateSystem{T}([1; 0; 0], [0; 1; 0], [0; 0; 1], I(3))

"""
    PrimitiveVectors(cc)

Generates vectors for a primitive cell on the basis of a coordinate system, like the `CartesianCoords`.
"""
function PrimitiveVectors(cc::CoordinateSystem{T};
        A₁=[1; 0; 0], A₂=[0; 1; 0], A₃=[0; 0; 1]) where {T<:Real}
    # x = SVector{3}(sum([cc.x, cc.y, cc.z] .* A₁))
    x = cc.M * A₁
    y = cc.M * A₂
    z = cc.M * A₃
    M = hcat(x,y,z)
    return CoordinateSystem{T}(x, y, z, M)
end

"""
    Atom(; name, mass)

Generates an atom object for the creation of unit cells.
"""
struct Atom{T}
    name::String
    mass::T
end

Atom(name,mass) = Atom{Float64}(name,mass)
make_atom(name,mass) = Atom{typeof(mass)}(name,mass)

function Atom(;name::String="dummy",mass::T=42) where T<:Real
    return Atom{typeof(mass)}(name, mass)
end

struct Spacegroup
    nr::Int32 #spacegroup number
    setting::Int8 # spacegroup setting (1 or 2, inherited from ase.spacegroup)
    kinds::Array{Int16,1}
    sites::Array{Any,1}
end

Spacegroup() = Spacegroup(-1,-1,[-1],[0])

struct Cell{T}
    atoms::Array
    positions
    box::CoordinateSystem{T}
    edge_lengths::Array{T}
    # spacegroup related infos
    spacegroup::Spacegroup
end

Cell(atoms, positions, box, edge_lengths) = Cell{Float64}(atoms, positions, box, edge_lengths, Spacegroup())
Cell(atoms, positions, box, edge_lengths, spg) = Cell{Float64}(atoms, positions, box, edge_lengths, spg)

"""
    make_unitcell(basis, symbols, nr, setting, cellpar; spg_file=nothing, a_direction=[1.; 0.; 0.], ab_normal=[0.;0.;1.], atom_cb=make_atom)

Creates a `Cell` struct including atom information, atom coordinates, the cell vectors/box and the edge lengths of the box.
"""
function make_unitcell(basis, symbols, nr, setting, cellpar;
        a_direction=[1.; 0.; 0.], ab_normal=[0.; 0.; 1.],
        atom_cb::Function=make_atom)
    # get constants
    chemical_symbols, atomic_numbers, masses = get_chemical_info()

    # get stored symmetry operation components
    spgs = load_spgs()
    spg = parse_json_spacegroup(spgs, nr, setting)

    # prepare symmetry operations for application to `basis`
    symops = get_symops(spg)

    # generating equivalent sites
    sites, kinds = get_equivalent_sites(basis, symops)

    # computing the simulation cell/box
    xyz = get_coords(a_direction, ab_normal)
    abc = get_cell_vectors(cellpar)
    cell = abc * xyz

    # storing crystal properties in a `SingleCrystal.Cell` instance
    el2atom_map = Dict(el => atom_cb(el, masses[el]) for el in symbols)

    cc = CartesianCoords(Float64)
    box = PrimitiveVectors(cc, A₁=abc[:,1], A₂=abc[:,2], A₃=abc[:,3])

    _spg = Spacegroup(nr, setting, kinds, sites)

    crystal = Cell(
        [el2atom_map[symbols[v]] for v in kinds],
        [cell * v for v in  sites],
        box,
        [norm(abc[:,1]), norm(abc[:,2]), norm(abc[:,3])],
        _spg
    )
    return crystal
end

"""
    make_bcc_unitcell(symbol, a)

Convenience function wrapping `make_unitcell` to create a bcc unit cell.
"""
make_bcc_unitcell(symbol::String,a::T,atom_cb::Function=make_atom) where T<:Real = make_unitcell([[0. 0. 0.],], [symbol], 229, 1, [a,a,a,90.,90,90], atom_cb=atom_cb)

"""
    make_fcc_unitcell(element, a)

Convenience function wrapping `make_unitcell` to create a fcc unit cell.
"""
make_fcc_unitcell(symbol::String,a::T,atom_cb::Function=make_atom) where T<:Real = make_unitcell([[0. 0. 0.],], [symbol], 225, 1, [a,a,a,90.,90,90], atom_cb=atom_cb)

"""
    add_vacancies(c, ixs; random=false, n_vac=1)

Adds a vacancy / removes an atom to/from a `Cell` struct.
"""
function add_vacancies(c::Cell; ixs::Array{Int}=[1], random::Bool=false, n_vac::Int=1)
    # if `random=true` then `n_vac` are generated and `ixs` is ignored
    n_atoms = length(c.atoms)
    if random == true
        ixs = rand(1:n_atoms, n_vac)
    end
    atoms_vac = [c.atoms[i] for i in 1:n_atoms if !(i in ixs)]
    positions_vac = [c.positions[i] for i in 1:n_atoms if !(i in ixs)]
    type = typeof(positions_vac[1][1])
    return Cell(atoms_vac, positions_vac, c.box, c.edge_lengths, c.spacegroup)
end

"""
    make_supercell(c;nx=1,ny=1,nz=1)

Creates a supercell by replicating the unitcell `c` and translating it along the vectors in `cell.box` `nx` * `ny` * `nz` times.
"""
function make_supercell(c::Cell; nx::I=1, ny::I=1, nz::I=1) where {I <: Integer}
    @assert (nx > 0) & (ny > 0) & (nz > 0)
    atoms = []
    positions = []
    box = PrimitiveVectors(c.box, A₁=[nx; 0; 0], A₂=[0; ny; 0], A₃=[0; 0; nz])
    edge_lengths = [norm(box.x), norm(box.y), norm(box.z)]

    for i in 0:nx-1, j in 0:ny-1, k in 0:nz-1
        shift = c.box.M * [i, j, k] # [c.box.x * i + c.box.y * j + c.box.z * k]
        new_pos = [pos + shift for pos in c.positions]
        push!(positions, new_pos) # TODO:  ((3, 1), Size(3,)) of input arrays do not match
        push!(atoms, c.atoms)
    end
    supercell = Cell(vcat(atoms...), vcat(positions...), box, edge_lengths)
    return supercell
end

"""
    vector1D(c1,c2,box_size)

Computes the periodic 1D distance between `c1` and `c2` with `box_size` as the periodic boundary.
"""
function vector1D(c1::Real, c2::Real, box_size::Real)
    if c1 < c2
        return (c2 - c1) < (c1 - c2 + box_size) ? (c2 - c1) : (c2 - c1 - box_size)
    else
        return (c1 - c2) < (c2 - c1 + box_size) ? (c2 - c1) : (c2 - c1 + box_size)
    end
end

"""
    vector(c1,c2,box_size)

Convenience function wrapping `vector1D` for the case of a cube.
"""
vector(c1, c2, box_size::Real) = vector1D.(c1, c2, box_size) # all box vectors orthogonal and of equal length

"""
    vector(c1,c2,edge_lengths)

Convenience function wrapping `vector1D` for the case of a non-cubic box with still orthogonal box vectors.
"""
vector(c1, c2, edge_lengths::Array) = vector1D.(c1, c2, edge_lengths) # all box vectors orthogonal

function parse_json_crystal(json_ase_crystal::Dict{String,Any})
    ase_crystal = Dict()
    ase_crystal["spacegroup_kinds"] = Array{Int32}(json_ase_crystal["spacegroup_kinds"])
    ase_crystal["positions"] = [Array{Float64}(v) for v in json_ase_crystal["positions"]]
    ase_crystal["cell"] = hcat(json_ase_crystal["cell"]...)
    ase_crystal["symbols"] = Array{String}(json_ase_crystal["symbols"])
    return ase_crystal
end

function cell2dict(crystal::SingleCrystal.Cell)

    chemical_symbols, atomic_numbers, masses = get_chemical_info()

    d = Dict()
    d["numbers"] = [atomic_numbers[atom.name] for atom in crystal.atoms] #[atomic_numbers[symbols[v]] for v in kinds]
    d["positions"] = hcat(crystal.positions...)
    d["spacegroup_kinds"] = crystal.spacegroup.kinds
    d["cell"] = crystal.box.M
    d["pbc"] = [true,true,true]
    d["info"] = Dict("spacegroup"=>Dict("nr"=>crystal.spacegroup.nr, "setting"=>crystal.spacegroup.setting))
    return d
end

end


# Common Utils
# ============
#
# Common utilities shared among several algorithms (internal use only).
#
# This file is a part of BioJulia.
# License is MIT: https://github.com/BioJulia/BioAlignments.jl/blob/master/LICENSE.md

# k: gap length
function affinegap_score(k, gap_open_penalty, gap_extend_penalty)
    return -(gap_open_penalty + gap_extend_penalty * k)
end

# Trace type
# ----------

# trace type for pairwise alignment
const Trace = UInt8

# trace bitmap
const TRACE_NONE   = 0b00000
const TRACE_MATCH  = 0b00001
const TRACE_DELETE = 0b00010
const TRACE_INSERT = 0b00100
const TRACE_EXTDEL = 0b01000
const TRACE_EXTINS = 0b10000

# Traceback
# ---------

macro start_traceback()
    esc(quote
        __alnpos = 0
        anchor_point = (i, j, __alnpos)
        op = OP_INVALID
    end)
end

# reverses the anchors sequence at the end of the traceback
# and offsets the alignment positions, so that (by default) it starts from 0
function reverse_anchors!(v::AbstractVector{AlignmentAnchor},
                          alignment_offset=!isempty(v) ? -v[end].alnpos : 0)
    r = n = length(v)
    @inbounds for i in 1:fld1(n, 2)
        vr = v[r]
        vi = v[i]
        v[i] = AlignmentAnchor(vr.seqpos, vr.refpos, vr.alnpos + alignment_offset, vr.op)
        (i != r) && (v[r] = AlignmentAnchor(vi.seqpos, vi.refpos, vi.alnpos + alignment_offset, vi.op))
        r -= 1
    end
    return v
end

macro finish_traceback()
    esc(quote
        push!(anchors, AlignmentAnchor(anchor_point..., op))
        push!(anchors, AlignmentAnchor(i, j, __alnpos, OP_START))
        reverse_anchors!(anchors)
        pop!(anchors)  # remove OP_INVALID
    end)
end

macro anchor(ex)
    esc(quote
        if op != $ex
            push!(anchors, AlignmentAnchor(anchor_point..., op))
            op = $ex
            anchor_point = (i, j, __alnpos)
        end
        __alnpos -= 1
        if ismatchop(op)
            i -= 1
            j -= 1
        elseif isinsertop(op)
            i -= 1
        elseif isdeleteop(op)
            j -= 1
        else
            @assert false
        end
    end)
end


module Simple

using StaticArrays: FieldVector
using DocStringExtensions: SIGNATURES

using ..DobotMagician: Magician, alarm_description
using ..Direct

export XYZR, XYZRL, Joint, JointL, MoveMode, MOVJ, MOVL, JUMP
export move_to,
    rmove_to,
    pose,
    pose_l,
    end_effector,
    laser,
    gripper,
    suction_cup,
    wait_for,
    alarm_description

"""
    XYZR

A structure to represent Cartesian coordinates `(x, y, z)` plus the end effector rotation
`r`.
"""
struct XYZR <: FieldVector{4,Float32}
    x::Float32
    y::Float32
    z::Float32
    r::Float32
end

"""
    XYZRL

A structure to represent Cartesian coordinates `(x, y, z)` plus the end effector rotation
`r` and the sliding rail displacement `l`.
"""
struct XYZRL <: FieldVector{5,Float32}
    x::Float32
    y::Float32
    z::Float32
    r::Float32
    l::Float32
end

"""
    Joint

A structure to represent joint coordinates `(J1, J2, J3, J4)`.
"""
struct Joint <: FieldVector{4,Float32}
    j1::Float32
    j2::Float32
    j3::Float32
    j4::Float32
end

"""
    JointL

A structure to represent joint coordinates `(J1, J2, J3, J4)` plus the sliding rail
displacement `l`.
"""
struct JointL <: FieldVector{5,Float32}
    j1::Float32
    j2::Float32
    j3::Float32
    j4::Float32
    l::Float32
end

abstract type MoveMode end

"""
    MOVJ

Linear movement in the joint coordinate system.
"""
struct MOVJ <: MoveMode end

"""
    MOVL

Linear movement in the Cartesian coordinate system.
"""
struct MOVL <: MoveMode end

"""
    JUMP

Move to `lifting_height` above the current point, execute a MOVL to `lifting_height` above
the destination point, and then descend to the destination point.

Also see `get_ptp_jump_params`, `set_ptp_jump_params`, `get_ptp_jump2_params`, and
`set_ptp_jump2_params`.
"""
struct JUMP <: MoveMode end

"""
    move_to(dobot, xyzr::Union{XYZR, XYZRL}, [::MoveMode=MOVL]; [queue=false])
    move_to(dobot, joint::Union{Joint, JointL}, [::MoveMode=MOVJ]; [queue=false])

Move to the specified position in Cartesian coordinates (`xyzr`) or in joint coordinates
(`joint`) using the specified `MoveMode`, where `MoveMode` is one of `MOVJ`, `MOVL`, or
`JUMP`.

Move commands can be queued or executed immediately. Note: it appears that commands executed
immediately are only successful if no other command is being carried out.
"""
function move_to end

"""
    rmove_to(dobot, xyzr::Union{XYZR, XYZRL}, [::MoveMode=MOVL]; [queue=false])
    rmove_to(dobot, joint::Union{Joint, JointL}, [::MoveMode=MOVJ]; [queue=false])

Move to the specified relative position in Cartesian coordinates (`xyzr`) or in joint
coordinates (`joint`) using the specified `MoveMode`, where `MoveMode` is one of `MOVJ`,
`MOVL`, or `JUMP`.

Move commands can be queued or executed immediately. Note: it appears that commands executed
immediately are only successful if no other command is being carried out.
"""
function rmove_to end

#! format: off

# Defaults
move_to(dobot::Magician, xyzr::Union{XYZR, XYZRL}; queue=false) = move_to(dobot, xyzr, MOVL; queue=queue)
move_to(dobot::Magician, joint::Union{Joint, JointL}; queue=false) = move_to(dobot, joint, MOVJ; queue=queue)

rmove_to(dobot::Magician, xyzr::XYZR; queue=false) = rmove_to(dobot, xyzr, MOVL; queue=queue)
rmove_to(dobot::Magician, joint::Joint; queue=false) = rmove_to(dobot, joint, MOVJ; queue=queue)

# ptp_mode: 0=JUMP_XYZ, 1=MOVJ_XYZ, 2=MOVL_XYZ, 3=JUMP_ANGLE, 4=MOVJ_ANGLE, 5=MOVL_ANGLE,
# 6=MOVJ_INC, 7=MOVL_INC, 8=MOVJ_XYZ_INC, 9=JUMP_MOVL_XYZ

move_to(dobot::Magician, xyzr::XYZR, ::Type{JUMP}; queue=false) = set_ptp_cmd(dobot, (0, xyzr); queue=queue)
move_to(dobot::Magician, xyzr::XYZR, ::Type{MOVJ}; queue=false) = set_ptp_cmd(dobot, (1, xyzr); queue=queue)
move_to(dobot::Magician, xyzr::XYZR, ::Type{MOVL}; queue=false) = set_ptp_cmd(dobot, (2, xyzr); queue=queue)
move_to(dobot::Magician, joint::Joint, ::Type{JUMP}; queue=false) = set_ptp_cmd(dobot, (3, joint); queue=queue)
move_to(dobot::Magician, joint::Joint, ::Type{MOVJ}; queue=false) = set_ptp_cmd(dobot, (4, joint); queue=queue)
move_to(dobot::Magician, joint::Joint, ::Type{MOVL}; queue=false) = set_ptp_cmd(dobot, (5, joint); queue=queue)

move_to(dobot::Magician, xyzrl::XYZRL, ::Type{JUMP}; queue=false) = set_ptp_with_l_cmd(dobot, (0, xyzrl); queue=queue)
move_to(dobot::Magician, xyzrl::XYZRL, ::Type{MOVJ}; queue=false) = set_ptp_with_l_cmd(dobot, (1, xyzrl); queue=queue)
move_to(dobot::Magician, xyzrl::XYZRL, ::Type{MOVL}; queue=false) = set_ptp_with_l_cmd(dobot, (2, xyzrl); queue=queue)
move_to(dobot::Magician, jointl::JointL, ::Type{JUMP}; queue=false) = set_ptp_with_l_cmd(dobot, (3, jointl); queue=queue)
move_to(dobot::Magician, jointl::JointL, ::Type{MOVJ}; queue=false) = set_ptp_with_l_cmd(dobot, (4, jointl); queue=queue)
move_to(dobot::Magician, jointl::JointL, ::Type{MOVL}; queue=false) = set_ptp_with_l_cmd(dobot, (5, jointl); queue=queue)

# Note: Dobot Magician API is rather confusing about the relative moves - these need checking...
rmove_to(dobot::Magician, joint::Joint, ::Type{MOVJ}; queue=false) = set_ptp_cmd(dobot, (6, joint); queue=queue)
rmove_to(dobot::Magician, xyzr::XYZR, ::Type{MOVL}; queue=false) = set_ptp_cmd(dobot, (7, xyzr); queue=queue)
rmove_to(dobot::Magician, xyzr::XYZR, ::Type{MOVJ}; queue=false) = set_ptp_cmd(dobot, (8, xyzr); queue=queue)
rmove_to(dobot::Magician, xyzr::XYZR, ::Type{JUMP}; queue=false) = set_ptp_cmd(dobot, (9, xyzr); queue=queue) # doesn't seem to work?

rmove_to(dobot::Magician, jointl::JointL, ::Type{MOVJ}; queue=false) = set_ptp_with_l_cmd(dobot, (6, jointl); queue=queue)
rmove_to(dobot::Magician, jointl::JointL, ::Type{MOVL}; queue=false) = set_ptp_with_l_cmd(dobot, (7, jointl); queue=queue)
rmove_to(dobot::Magician, xyzrl::XYZRL, ::Type{MOVJ}; queue=false) = set_ptp_with_l_cmd(dobot, (8, xyzrl); queue=queue)
rmove_to(dobot::Magician, xyzrl::XYZRL, ::Type{JUMP}; queue=false) = set_ptp_with_l_cmd(dobot, (9, xyzrl); queue=queue)

#! format: on

"""
    $(SIGNATURES)

Return the pose of the Dobot in the specified coordinate frame. The coordinate frame can be
`XYZR` or `Joint`; if the sliding rail attachment is used, the corresponding coordinate
frames are `XYZRL` and `JointL`.
"""
pose(dobot::Magician, coordinates::Type{XYZR}) = XYZR(get_pose(dobot)[1])
pose(dobot::Magician, ::Type{Joint}) = Joint(get_pose(dobot)[2])
pose(dobot::Magician, ::Type{XYZRL}) = XYZRL(get_pose(dobot)[1]..., get_pose_l(dobot)[1])
pose(dobot::Magician, ::Type{JointL}) = JointL(get_pose(dobot)[2]..., get_pose_l(dobot)[1])

"""
    $(SIGNATURES)

Enable control of a particular end effector. Valid values for `effector` are

- `:laser`
- `:gripper`
- `:suction_cup`
"""
function end_effector(dobot::Magician, effector::Symbol, ctrl::Bool=true; queue=false)
    if effector === :laser
        set_end_effector_laser(dobot, (UInt8(ctrl), 0); queue=queue)
    elseif effector === :gripper
        set_end_effector_gripper(dobot, (UInt8(ctrl), 0); queue=queue)
    elseif effector === :suction_cup
        set_end_effector_suction_cup(dobot, (UInt8(ctrl), 0); queue=queue)
    else
        throw(ArgumentError("Unknown end effector"))
    end
    return nothing
end

"""
    $(SIGNATURES)

Return true if the laser is on, otherwise return false.
"""
function laser(dobot::Magician)
    (ctrl, on) = get_end_effector_laser(dobot)
    return (ctrl == 0x1) && (on == 0x1)
end

"""
    $(SIGNATURES)

Turn the laser on or off.
"""
function laser(dobot::Magician, status::Bool; queue=false)
    return set_end_effector_laser(dobot, (1, UInt8(status)); queue=queue)
end

"""
    $(SIGNATURES)

Return true if the gripper is on, otherwise return false.
"""
function gripper(dobot::Magician)
    (ctrl, on) = get_end_effector_gripper(dobot)
    return (ctrl == 0x1) && (on == 0x1)
end

"""
    $(SIGNATURES)

Turn the gripper on or off.
"""
function gripper(dobot::Magician, status::Bool; queue=false)
    return set_end_effector_gripper(dobot, (1, UInt8(status)); queue=queue)
end

"""
    $(SIGNATURES)

Return true if the suction_cup is on, otherwise return false.
"""
function suction_cup(dobot::Magician)
    (ctrl, on) = get_end_effector_suction_cup(dobot)
    return (ctrl == 0x1) && (on == 0x1)
end

"""
    $(SIGNATURES)

Turn the suction_cup on or off.
"""
function suction_cup(dobot::Magician, status::Bool; queue=false)
    return set_end_effector_suction_cup(dobot, (1, UInt8(status)); queue=queue)
end

"""
    $(SIGNATURES)

Wait for `timeout` milliseconds.
"""
function wait_for(dobot::Magician, timeout::Integer; queue=false)
    return set_wait_cmd(dobot, (timeout,); queue=queue)
end

end  # module