"""
`module  JAC.RayleighCompton`
    ... a submodel of JAC that contains all methods for computing elastic Rayleigh and inelastic Compton photon scattering cross sections.
"""
module RayleighCompton

    using Printf, ..AngularMomentum, ..AtomicState, ..Basics, ..Defaults, ..ManyElectron, ..Radial, ..TableStrings

    """
    `struct  RayleighCompton.Settings`  ... defines a type for the settings in estimating lastic Rayleigh and inelastic Compton
                                            photon scattering cross sections

        + multipoles              ::Array{EmMultipole}      ... Specifies the multipoles of the radiation field that are to be included.
        + gauges                  ::Array{UseGauge}         ... Specifies the gauges to be included into the computations.
        + photonEnergies          ::Array{Float64,1}        ... List of photon energies in units defined by the user.
        + green                   ::Array{GreenChannel,1}   ... Precalculated and user-specified Green function of the ion.
        + calcAngular             ::Bool                    ... Calculate the angular distribution of the fluorescence photon.
        + calcStokes              ::Bool                    ... Calculate the Stokes parameters of the Rayleigh-scattered photon.
        + printBefore             ::Bool                    ... True, if all energies and lines are printed before their evaluation.
        + incidentStokes          ::ExpStokes               ... Stokes parameters of the incident radiation.
        + solidAngles             ::Array{SolidAngle,1}     ... List of solid angles [(theta_1, pho_1), ...].
        + lineSelection           ::LineSelection           ... Specifies the selected levels, if any.

    """
    struct Settings
        multipoles                ::Array{EmMultipole}
        gauges                    ::Array{UseGauge}
        photonEnergies            ::Array{Float64,1}
        green                     ::Array{AtomicState.GreenChannel,1}
        calcAngular               ::Bool
        calcStokes                ::Bool
        printBefore               ::Bool
        incidentStokes            ::ExpStokes
        solidAngles               ::Array{SolidAngle,1}
        lineSelection             ::LineSelection
    end

    """
    `RayleighCompton.Settings()`  ... constructor for the default values of Rayleigh-Compton photon-scattering estimates.
    """
    function Settings()
        Settings(EmMultipole[], UseGauge[], GreenChannel[], 0., false, false, false, ExpStokes(), SolidAngle[], LineSelection() )
    end

    # `Base.show(io::IO, settings::RayleighCompton.Settings)`  ... prepares a proper printout of the variable settings::RayleighCompton.Settings.
    function Base.show(io::IO, settings::RayleighCompton.Settings)
        println(io, "multipoles:               $(settings.multipoles)  ")
        println(io, "gauges:                   $(settings.gauges)  ")
        println(io, "photonEnergies:           $(settings.photonEnergies)  ")
        println(io, "green:                     (settings.green)  ")
        println(io, "calcAngular:              $(settings.calcAngular)  ")
        println(io, "calcStokes:               $(settings.calcStokes)  ")
        println(io, "printBefore:              $(settings.printBefore)  ")
        println(io, "incidentStokes:           $(settings.incidentStokes)  ")
        println(io, "solidAngles:              $(settings.solidAngles)  ")
        println(io, "lineSelection:            $(settings.lineSelection)  ")
    end

    """
    `struct  RayleighCompton.ReducedChannel`
        ... defines a type for a Rayleigh-Compton reduced channel/ U^(K, Compton) amplitude to help characterize the scattering of
            a single photon with well-defined multipolarities; in JAC, a Rayleigh-Compton channel is characterized by a reduced amplitude
            U^(K, Compton) (level_f, multipole_2, J_nu, omega, multipole_1, level_i).

        + K              ::AngularJ64           ... Rank K of the channel.
        + multipole1     ::EmMultipole          ... Multipole of the incoming photon.
        + multipole2     ::EmMultipole          ... Multipole of the outgoing, scattered photon.
        + gauge          ::EmGauge              ... Gauge for dealing with the (coupled) radiation field.
        + omega1         ::Float64              ... Photon frequency of multipole1.
        + omega2         ::Float64              ... Photon frequency of multipole2.
        + omega          ::Float64              ... Photon frequency in denominator.
        + Jsym           ::LevelSymmetry        ... Symmetry J_nu of the intermediate levels.
        + amplitude      ::Complex{Float64}     ... Two-photon reduced amplitude associated with the given channel.
   """
    struct  ReducedChannel
        K                ::AngularJ64
        multipole1       ::EmMultipole
        multipole2       ::EmMultipole
        gauge            ::EmGauge
        omega1           ::Float64
        omega2           ::Float64
        omega            ::Float64
        Jsym             ::LevelSymmetry
        amplitude        ::Complex{Float64}
    end

    """
    `struct  RayleighCompton.Line`  ... defines a type for a Rayleigh-Compton scattering line that may include the definition of channels.

        + initialLevel     ::Level              ... initial-(state) level
        + finalLevel       ::Level              ... final-(state) level
        + inOmega          ::Float64            ... Energy of the incoming photon.
        + outOmega         ::Float64            ... Energy of the outgoing, scattered photon.
        + crossSection     ::EmProperty         ... Cross section for this photoionization.
        + channels         ::Array{RayleighCompton.ReducedChannel,1}    ... List of (reduced) RayleighCompton.Channels of this line.
    """
    struct  Line
        initialLevel       ::Level
        finalLevel         ::Level
        inOmega            ::Float64
        outOmega           ::Float64
        crossSection       ::EmProperty
        channels           ::Array{RayleighCompton.ReducedChannel,1}
    end

    # `Base.show(io::IO, line::RayleighCompton.Line)`  ... prepares a proper printout of the variable line::RayleighCompton.Line.
    function Base.show(io::IO, line::RayleighCompton.Line)
        println(io, "initialLevel:      $(line.initialLevel)  ")
        println(io, "finalLevel:        $(line.finalLevel)  ")
        println(io, "inOmega:           $(line.inOmega)  ")
        println(io, "outOmega:          $(line.outOmega)  ")
        println(io, "crossSection:      $(line.crossSection)  ")
        println(io, "channels:          $(line.channels)  ")
    end

    """
    `RayleighCompton.computeAmplitudesProperties(line::RayleighCompton.Line, grid::Radial.Grid, settings::RayleighCompton.Settings)`
        ... to compute all amplitudes and properties of the given line; a line::RayleighCompton.Line is returned for which
            the amplitudes and properties are now evaluated.
    """
    function  computeAmplitudesProperties(line::RayleighCompton.Line, grid::Radial.Grid, settings::RayleighCompton.Settings)
        newChannels = RayleighCompton.ReducedChannel[]
        for ch in line.channels
            amplitude = RayleighCompton.computeReducedAmplitude(ch.K, line.finalLevel, ch.multipole2, ch.omega2, ch.Jsym, ch.omega,
                                                                ch.multipole1, ch.omega1, line.initialLevel, ch.gauge, grid, settings.green)
            push!( newChannels, ReducedChannel(ch.K, ch.multipole1, ch.multipole2, ch.gauge, ch.omega1, ch.omega2, ch.omega, ch.Jsym, amplitude) )
        end
        cs_Cou       = 0.;   cs_Bab = 0.
        crossSection = EmProperty(cs_Cou, cs_Bab)
        newLine      = RayleighCompton.Line(line.initialLevel, line.finalLevel, line.inOmega, line.outOmega, crossSection, newChannels)
        return( newLine )
    end

    """
    `RayleighCompton.computeLines(finalMultiplet::Multiplet, initialMultiplet::Multiplet, grid::Radial.Grid, settings::RayleighCompton.Settings; output=true)`
        ... to compute the Rayleigh-Compton transition amplitudes and all properties as requested by the given settings. A list of
            lines::Array{RayleighCompton.Lines} is returned.
    """
    function  computeLines(finalMultiplet::Multiplet, initialMultiplet::Multiplet, grid::Radial.Grid, settings::RayleighCompton.Settings; output=true)
        println("")
        printstyled("RayleighCompton.computeLines(): The computation of Rayleigh-Compton reduced amplitudes starts now ... \n", color=:light_green)
        printstyled("----------------------------------------------------------------------------------------------------- \n", color=:light_green)
        println("")
        #
        lines = RayleighCompton.determineLines(finalMultiplet, initialMultiplet, settings)
        # Display all selected pathways before the computations start
        if  settings.printBefore    RayleighCompton.displayLines(stdout, lines)    end
        # Calculate all reduced amplitudes and requested properties
        newLines = RayleighCompton.Line[]
        for  line in lines
            newLine = RayleighCompton.computeAmplitudesProperties(line, grid, settings)
            push!( newLines, newLine)
        end
        # Print all results to screen
        RayleighCompton.displayCrossSections(stdout, newLines, settings)
        if  settings.calcAngular        RayleighCompton.displayAngular(stdout, newLines, settings)             end
        if  settings.calcStokes         RayleighCompton.displayStokesParameters(stdout, newLines, settings)    end
        printSummary, iostream = Defaults.getDefaults("summary flag/stream")
        if  printSummary
            if  settings.calcAngular    RayleighCompton.displayAngular(stdout, newLines, settings)             end
            if  settings.calcStokes     RayleighCompton.displayStokesParameters(stdout, newLines, settings)    end
        end

        if    output    return( lines )
        else            return( nothing )
        end
    end

    """
    `RayleighCompton.computeReducedAmplitude(K::AngularJ64, finalLevel::Level, multipole2::EmMultipole, omega2::Float64, Jsym::LevelSymmetry,
                                             omega::Float64, multipole1::EmMultipole, omega1::Float64, initialLevel::Level, gauge::EmGauge,
                                             grid::Radial.Grid, greenChannels::Array{AtomicState.GreenChannel,1})`
        ... to compute the reduced amplitudes U^(K, Compton) as well as all properties of the given line;
            a line::RayleighCompton.Line is returned for which the reduced amplitudes and properties are now evaluated.
    """
    function  computeReducedAmplitude(K::AngularJ64, finalLevel::Level, multipole2::EmMultipole, omega2::Float64, Jsym::LevelSymmetry,
                                      omega::Float64, multipole1::EmMultipole, omega1::Float64, initialLevel::Level, gauge::EmGauge,
                                      grid::Radial.Grid, greenChannels::Array{AtomicState.GreenChannel,1})
        amplitude = 0.0im
        return( amplitude )
    end

    """
    `RayleighCompton.determineChannels(finalLevel::Level, initialLevel::Level, inOmega::Float64, outOmega::Float64, settings::RayleighCompton.Settings)`
        ... to determine a list of Rayleigh-Compton ReducedChannelChannel's for a transitions from the initial to final level and by taking
            into account the particular settings of for this computation; an Array{RayleighCompton.ReducedChannelChannel,1} is returned.
    """
    function determineChannels(finalLevel::Level, initialLevel::Level, inOmega::Float64, outOmega::Float64, settings::RayleighCompton.Settings)
        channels = RayleighCompton.ReducedChannel[];
        symi = LevelSymmetry(initialLevel.J, initialLevel.parity);    symf = LevelSymmetry(finalLevel.J, finalLevel.parity)
        for  mp1 in settings.multipoles
            for  mp2 in settings.multipoles
                symmetries  = AngularMomentum.allowedTotalSymmetries(symf, mp2, mp1, symi)
                Klist       = oplus(symf.J, symi.J)
                for  symn in symmetries
                    for  gauge in settings.gauges
                        # Include further restrictions if appropriate
                        if     string(mp1)[1] == 'E' || string(mp2)[1] == 'E'  &&   gauge == Basics.UseCoulomb
                            for K in Klist  push!(channels, ReducedChannel(K, mp1, mp2, Basics.Coulomb,   inOmega, outOmega, inOmega, symn, 0.) )  end
                        elseif string(mp1)[1] == 'E' || string(mp2)[1] == 'E'  &&   gauge == Basics.UseBabushkin
                            for K in Klist  push!(channels, ReducedChannel(K, mp1, mp2, Basics.Babushkin, inOmega, outOmega, inOmega, symn, 0.) )  end
                        elseif string(mp1)[1] == 'M' && string(mp2)[1] == 'M'
                            for K in Klist  push!(channels, ReducedChannel(K, mp1, mp2, Basics.Magnetic,  inOmega, outOmega, inOmega, symn, 0.) )  end
                        end
                    end
                end
            end
        end
        # Unify channels
        return( channels )
    end

    """
    `RayleighCompton.determineLines(finalMultiplet::Multiplet, initialMultiplet::Multiplet, settings::RayleighCompton.Settings)`
        ... to determine a list of RayleighCompton Line's for elastic scattering or Raman-type transitions between the levels from
            the given initial- and final-state multiplets and by taking into account the particular selections and settings for
            this computation; an Array{RayleighCompton.Line,1} is returned. Apart from the level specification, all physical
            properties are set to zero during the initialization process.
    """
    function  determineLines(finalMultiplet::Multiplet, initialMultiplet::Multiplet, settings::RayleighCompton.Settings)
        lines = RayleighCompton.Line[]
        for  iLevel  in  initialMultiplet.levels
            for  fLevel  in  finalMultiplet.levels
                if  Basics.selectLevelPair(iLevel, fLevel, settings.lineSelection)
                    for  omega in settings.photonEnergies
                        inOmega  = Defaults.convertUnits("energy: to atomic", omega)
                        outOmega = inOmega - (fLevel.energy - iLevel.energy);       if  outOmega <= 0.    continue   end
                        channels = RayleighCompton.determineChannels(fLevel, iLevel, inOmega, outOmega, settings)
                        if   length(channels) == 0   continue   end
                        push!( lines, RayleighCompton.Line(iLevel, fLevel, inOmega, outOmega, EmProperty(0., 0.), channels) )
                    end
                end
            end
        end
        return( lines )
    end

    """
    `RayleighCompton.displayAngular(stream::IO, lines::Array{RayleighCompton.Line,1}, settings::RayleighCompton.Settings)`
        ... to display a list of lines and the angle-differential cross sections for the selected solid angles.
            A neat table of all selected transitions, energies and solid angles is printed but nothing is returned otherwise.
    """
    function  displayAngular(stream::IO, lines::Array{RayleighCompton.Line,1}, settings::RayleighCompton.Settings)
        nx = 127
        println(stream, " ")
        println(stream, "  Angular distribution of the selected Rayleigh-Compton scattering lines at seleced solid angles: ... to be adapted !!")
        println(stream, " ")
        println(stream, "  ", TableStrings.hLine(nx))
        sa = "   ";   sb = "   ";   sc = "  "
        sa = sa * TableStrings.center(16, "i-level-f"; na=2);                         sb = sb * TableStrings.hBlank(18)
        sa = sa * TableStrings.center(16, "i--J^P--f"; na=7);                         sb = sb * TableStrings.hBlank(23)
        sa = sa * TableStrings.center(20, "i -- Energy -- f"; na=3);
        sb = sb * TableStrings.center(20, TableStrings.inUnits("energy"); na=5)
        sa = sa * TableStrings.center(24, "omega-in   omega-out"; na=2);
        sb = sb * TableStrings.center(10, TableStrings.inUnits("energy"); na=1)
        sb = sb * TableStrings.center(10, TableStrings.inUnits("energy"); na=6)
        println(stream, sa[1:end]);    println(stream, sb);    println(stream, "  ", TableStrings.hLine(nx))
        #
        for  line in lines
            sa  = "";    isym = LevelSymmetry( line.initialLevel.J, line.initialLevel.parity)
                         fsym = LevelSymmetry( line.finalLevel.J,   line.finalLevel.parity)
            sa = sa * TableStrings.center(18, TableStrings.levels_if(line.initialLevel.index, line.finalLevel.index); na=2)
            sa = sa * TableStrings.center(18, TableStrings.symmetries_if(isym, fsym); na=4)
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.initialLevel.energy)) * " "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.finalLevel.energy))   * "   "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.inOmega))      * " "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.outOmega))     * "       "
            println(stream, sa )
        end
        println("  ", TableStrings.hLine(nx))
        #
        return( nothing )
    end

    """
    `RayleighCompton.displayCrossSections(stream::IO, lines::Array{RayleighCompton.Line,1}, settings::RayleighCompton.Settings)`
        ... to display a list of lines and their cross sections as they were selected by the settings. A neat table of all selected
            transitions, energies and cross sections is printed but nothing is returned otherwise.
    """
    function  displayCrossSections(stream::IO, lines::Array{RayleighCompton.Line,1}, settings::RayleighCompton.Settings)
        nx = 127
        println(stream, " ")
        println(stream, "  Cross sections of the selected Rayleigh-Compton scattering lines:")
        println(stream, " ")
        println(stream, "  ", TableStrings.hLine(nx))
        sa = "   ";   sb = "   ";   sc = "  "
        sa = sa * TableStrings.center(16, "i-level-f"; na=2);                         sb = sb * TableStrings.hBlank(18)
        sa = sa * TableStrings.center(16, "i--J^P--f"; na=7);                         sb = sb * TableStrings.hBlank(23)
        sa = sa * TableStrings.center(20, "i -- Energy -- f"; na=3);
        sb = sb * TableStrings.center(20, TableStrings.inUnits("energy"); na=5)
        sa = sa * TableStrings.center(24, "omega-in   omega-out"; na=2);
        sb = sb * TableStrings.center(10, TableStrings.inUnits("energy"); na=1)
        sb = sb * TableStrings.center(10, TableStrings.inUnits("energy"); na=6)
        sa = sa * TableStrings.center(28, "Cou -- cross section -- Bab"; na=0);
        sb = sb * TableStrings.center(10, TableStrings.inUnits("cross section"); na=3)
        sb = sb * TableStrings.center(10, TableStrings.inUnits("cross section"); na=4)
        println(stream, sa[1:end]);    println(stream, sb);    println(stream, "  ", TableStrings.hLine(nx))
        #
        for  line in lines
            sa  = "";    isym = LevelSymmetry( line.initialLevel.J, line.initialLevel.parity)
                         fsym = LevelSymmetry( line.finalLevel.J,   line.finalLevel.parity)
            sa = sa * TableStrings.center(18, TableStrings.levels_if(line.initialLevel.index, line.finalLevel.index); na=2)
            sa = sa * TableStrings.center(18, TableStrings.symmetries_if(isym, fsym); na=4)
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.initialLevel.energy)) * " "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.finalLevel.energy))   * "   "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.inOmega))      * " "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.outOmega))     * "       "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("cross section: from atomic", line.crossSection.Coulomb))       * "   "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("cross section: from atomic", line.crossSection.Babushkin))     * "   "
            println(stream, sa )
        end
        println("  ", TableStrings.hLine(nx))
        #
        return( nothing )
    end

    """
    `RayleighCompton.displayLines(stream::IO, lines::Array{RayleighCompton.Line,1})`
        ... to display a list of lines and channels that have been selected due to the prior settings. A neat table of all selected
            transitions and energies is printed but nothing is returned otherwise.
    """
    function  displayLines(stream::IO, lines::Array{RayleighCompton.Line,1})
        nx = 157
        println(stream, " ")
        println(stream, "  Selected Rayleigh-Compton scacttering lines and (reduced) channels:")
        println(stream, " ")
        println(stream, "  ", TableStrings.hLine(nx))
        sa = "   ";   sb = "   ";   sc = "  "
        sa = sa * TableStrings.center(16, "i-level-f"; na=2);                         sb = sb * TableStrings.hBlank(18)
        sa = sa * TableStrings.center(16, "i--J^P--f"; na=7);                         sb = sb * TableStrings.hBlank(23)
        sa = sa * TableStrings.center(20, "i -- Energy -- f"; na=3);
        sb = sb * TableStrings.center(20, TableStrings.inUnits("energy"); na=5)
        sa = sa * TableStrings.center(24, "omega-in   omega-out"; na=0);
        sb = sb * TableStrings.center(10, TableStrings.inUnits("energy"); na=1)
        sb = sb * TableStrings.center(10, TableStrings.inUnits("energy"); na=4)
        sa = sa * TableStrings.center(11, "No channels"; na=2);                       sb = sb * TableStrings.hBlank(11)
        sa = sa * TableStrings.flushleft(60, "List of multipoles & intermediate level symmetries";   na=1)
        sb = sb * TableStrings.flushleft(60, "(K-rank, multipole_1, Jsym, multipole_2, gauge), ..."; na=1)
        println(stream, sa[1:end]);    println(stream, sb);    println(stream, "  ", TableStrings.hLine(nx))
        #
        for  line in lines
            sa  = "";    isym = LevelSymmetry( line.initialLevel.J, line.initialLevel.parity)
                         fsym = LevelSymmetry( line.finalLevel.J,   line.finalLevel.parity)
            sa = sa * TableStrings.center(18, TableStrings.levels_if(line.initialLevel.index, line.finalLevel.index); na=2)
            sa = sa * TableStrings.center(18, TableStrings.symmetries_if(isym, fsym); na=4)
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.initialLevel.energy)) * " "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.finalLevel.energy))   * "   "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.inOmega))      * " "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.outOmega))     * "   "
            sc = "         " * string(length(line.channels)) * "      "
            sa = sa * sc[end-12:end]
            mpGaugeList = Tuple{AngularJ64, Basics.EmMultipole, LevelSymmetry, Basics.EmMultipole, Basics.EmGauge}[]
            for  channel in  line.channels
                push!( mpGaugeList, (channel.K, channel.multipole1, channel.Jsym, channel.multipole2, channel.gauge) )
            end
            wa = TableStrings.twoPhotonGaugeTupels(60, mpGaugeList)
            if  length(wa) > 0    sb = sa * wa[1];    println( sb )    end
            for  i = 1:length(wa)
                sb = TableStrings.hBlank( length(sa) );    sb = sb * wa[i];    println(stream, sb )
            end
        end
        println("  ", TableStrings.hLine(nx))
        #
        return( nothing )
    end

    """
    `RayleighCompton.displayStokesParameters(stream::IO, lines::Array{RayleighCompton.Line,1}, settings::RayleighCompton.Settings)`
        ... to display a list of lines and the Stokes parameters at the selected solid angles.
            A neat table of all selected transitions, energies and solid angles is printed but nothing is returned otherwise.
    """
    function  displayStokesParameters(stream::IO, lines::Array{RayleighCompton.Line,1}, settings::RayleighCompton.Settings)
        nx = 127
        println(stream, " ")
        println(stream, "  StokesParameters of the selected Rayleigh-Compton scattering lines at seleced solid angles: ... to be adapted !!")
        println(stream, " ")
        println(stream, "  ", TableStrings.hLine(nx))
        sa = "   ";   sb = "   ";   sc = "  "
        sa = sa * TableStrings.center(16, "i-level-f"; na=2);                         sb = sb * TableStrings.hBlank(18)
        sa = sa * TableStrings.center(16, "i--J^P--f"; na=7);                         sb = sb * TableStrings.hBlank(23)
        sa = sa * TableStrings.center(20, "i -- Energy -- f"; na=3);
        sb = sb * TableStrings.center(20, TableStrings.inUnits("energy"); na=5)
        sa = sa * TableStrings.center(24, "omega-in   omega-out"; na=2);
        sb = sb * TableStrings.center(10, TableStrings.inUnits("energy"); na=1)
        sb = sb * TableStrings.center(10, TableStrings.inUnits("energy"); na=6)
        println(stream, sa[1:end]);    println(stream, sb);    println(stream, "  ", TableStrings.hLine(nx))
        #
        for  line in lines
            sa  = "";    isym = LevelSymmetry( line.initialLevel.J, line.initialLevel.parity)
                         fsym = LevelSymmetry( line.finalLevel.J,   line.finalLevel.parity)
            sa = sa * TableStrings.center(18, TableStrings.levels_if(line.initialLevel.index, line.finalLevel.index); na=2)
            sa = sa * TableStrings.center(18, TableStrings.symmetries_if(isym, fsym); na=4)
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.initialLevel.energy)) * " "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.finalLevel.energy))   * "   "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.inOmega))      * " "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.outOmega))     * "       "
            println(stream, sa )
        end
        println("  ", TableStrings.hLine(nx))
        #
        return( nothing )
    end

end # module


"""
Function to obtain series of states out of DAE Solution. It receives the solution, the dynamical system
and a tuple containing the string name of the Dynamic Injection device and the symbol of the state.

"""
function get_state_series(sim::Simulation, ref::Tuple{String, Symbol})
    return sim.solution.t, post_proc_state_series(sim, ref)
end

"""
Function to obtain the voltage magnitude series out of the DAE Solution. It receives the solution, the dynamical system and the bus number.

"""
function get_voltagemag_series(sim::Simulation, bus_number::Int)
    n_buses = get_bus_count(sim.simulation_inputs)
    bus_ix = get(get_lookup(sim.simulation_inputs), bus_number, 0)
    V_R, V_I = post_proc_voltage_series(sim.solution, bus_ix, n_buses)
    return sim.solution.t, sqrt.(V_R .^ 2 + V_I .^ 2)
end

"""
Function to obtain the active power output time series of a Dynamic Injection series out of the DAE Solution. It receives the solution and the
string name of the Dynamic Injection device.

"""
function get_activepower_series(sim::Simulation, name::String)
    return sim.solution.t, post_proc_activepower_series(sim, name)
end

"""
Function to obtain the reactive power output time series of a Dynamic Injection series out of the DAE Solution. It receives the solution and the
string name of the Dynamic Injection device.

"""
function get_reactivepower_series(sim::Simulation, name::String)
    return sim.solution.t, post_proc_reactivepower_series(sim, name)
end

"""
Function to print initial states. It receives the vector of initial states and the dynamical system.
"""
function print_device_states(sim::Simulation)
    bus_size = get_bus_count(sim.simulation_inputs)
    system = get_system(sim)
    println("Voltage Variables")
    println("====================")
    buses_sorted =
        sort(collect(PSY.get_components(PSY.Bus, system)); by = x -> PSY.get_number(x))
    for bus in buses_sorted
        name = PSY.get_name(bus)
        println(name)
        println("====================")
        bus_n = PSY.get_number(bus)
        bus_ix = get_lookup(sim.simulation_inputs)[bus_n]
        V_R = sim.x0_init[bus_ix]
        V_I = sim.x0_init[bus_ix + bus_size]
        Vm = sqrt(V_R^2 + V_I^2)
        θ = angle(V_R + V_I * 1im)
        print("Vm ", round(Vm, digits = 4), "\n")
        print("θ ", round(θ, digits = 4), "\n")
        println("====================")
    end
    println("====================")
    for device in PSY.get_components(PSY.DynamicInjection, system)
        states = PSY.get_states(device)
        name = PSY.get_name(device)
        println("Differential States")
        println(name)
        println("====================")
        global_index = get_global_index(sim.simulation_inputs)[name]
        for s in states
            print(s, " ", round(sim.x0_init[global_index[s]], digits = 4), "\n")
        end
        println("====================")
    end
    dyn_branches = PSY.get_components(PSY.DynamicBranch, system)
    if !isempty(dyn_branches)
        println("====================")
        println("Line Current States")
        for br in dyn_branches
            states = PSY.get_states(br)
            name = PSY.get_name(br)
            printed_name = "Line " * name
            println("====================")
            println(printed_name)
            global_index = get_global_index(sim.simulation_inputs)[name]
            x0_br = Dict{Symbol, Float64}()
            for (i, s) in enumerate(states)
                print(s, " ", round(sim.x0_init[global_index[s]], digits = 5), "\n")
            end
            println("====================")
        end
    end
    return
end


# Test that a hitran fixed-width file is correctly parsed
@testset "read_hitran" begin

    println("Testing read_hitran...")

    ####
    #### These tests check that the correct values & types are extracted from the file,
    #### especially that molecule/isotope match and ν between ν_min and ν_max)
    ####

    CO2_test_file = "test_profiles/testCO2.data"

    test_ht = Absorption.read_hitran(CO2_test_file, mol=2, iso=1, ν_min=6000, ν_max=6400)
    @test test_ht.mol == [2, 2, 2, 2] && test_ht.mol[1] isa Int64
    @test test_ht.iso == [1, 1, 1, 1] && test_ht.iso[1] isa Int64
    @test test_ht.νᵢ == [6000.542970, 6286.403343, 6317.417493, 6380.824116] && test_ht.νᵢ[1] isa Float64
    @test test_ht.Sᵢ == [ 1.098E-28, 9.843E-30, 5.613E-27, 1.809E-30] && test_ht.Sᵢ[1] isa Float64
    @test test_ht.Aᵢ == [ 9.993e-08, 1.179e-08, 1.324e-05, 1.601e-02] && test_ht.Aᵢ[1] isa Float64
    @test test_ht.γ_air == [.0880, .0687, .0682, .0671] && test_ht.γ_air[1] isa Float64
    @test test_ht.γ_self == [0.118, 0.087, 0.081, 0.073] && test_ht.γ_self[1] isa Float64
    @test test_ht.E″ == [7.8043, 464.1717, 639.6004, 3798.2095] && test_ht.E″[1] isa Float64
    @test test_ht.n_air == [0.77, 0.76, 0.76, 0.73] && test_ht.n_air[1] isa Float64
    @test test_ht.δ_air == [-.004342, -.007362, -.007443, -.007669] && test_ht.δ_air[1] isa Float64
    @test test_ht.global_upper_quanta == ["       4 1 1 03", "       2 2 2 12", "       2 2 2 12", "       4 2 2 12"] && test_ht.global_upper_quanta[1] isa String
    @test test_ht.global_lower_quanta == ["       0 0 0 01", "       0 0 0 01", "       0 0 0 01", "       1 2 2 01"] && test_ht.global_lower_quanta[1] isa String
    @test test_ht.local_upper_quanta == ["               ", "               ", "               ", "               "] && test_ht.local_upper_quanta[1] isa String
    @test test_ht.local_lower_quanta == ["     Q  4e     ", "     Q 34e     ", "     R 40e     ", "     R 51f     "] && test_ht.local_lower_quanta[1] isa String
    @test test_ht.ierr == ["367774", "367764", "367764", "367774"] && test_ht.ierr[1] isa String
    @test test_ht.iref == ["2029 5 4 5 7", "2029 5 4 5 7", "2029 5 4 5 7", "2029 5 4 5 7"] && test_ht.iref[1] isa String
    @test test_ht.line_mixing_flag == [" ", " ", " ", " "] && test_ht.line_mixing_flag[1] isa String
    @test test_ht.g′ == [9.0, 69.0, 83.0, 105.0] && test_ht.g′[1] isa Float64
    @test test_ht.g″ == [9.0, 69.0, 81.0, 103.0] && test_ht.g″[1] isa Float64

    ####
    #### These tests check that the optional params work correctly
    ####

    # Not specifying the molecule #
    test_ht = Absorption.read_hitran(CO2_test_file, iso=1, ν_min=6000, ν_max=6400)
    @test test_ht.mol == [1, 2, 2, 2, 2]
    @test test_ht.iso == [1, 1, 1, 1, 1]
    @test test_ht.νᵢ == [6286.403343, 6000.542970, 6286.403343, 6317.417493, 6380.824116]
    @test test_ht.g″ == [69.0, 9.0, 69.0, 81.0, 103.0]

    # Not specifying the isotope #
    test_ht = Absorption.read_hitran(CO2_test_file, mol=2, ν_min=6000, ν_max=6400)
    @test test_ht.mol == [2, 2, 2, 2, 2]
    @test test_ht.iso == [2, 1, 1, 1, 1]
    @test test_ht.νᵢ == [6000.542970, 6000.542970, 6286.403343, 6317.417493, 6380.824116]
    @test test_ht.g″ == [9.0, 9.0, 69.0, 81.0, 103.0]

    # Not specifying the molecule # OR isotope #
    test_ht = Absorption.read_hitran(CO2_test_file, ν_min=6000, ν_max=6400)
    @test test_ht.mol == [1, 2, 2, 2, 2, 2]
    @test test_ht.iso == [1, 2, 1, 1, 1, 1]
    @test test_ht.νᵢ == [6286.403343, 6000.542970, 6000.542970, 6286.403343, 6317.417493, 6380.824116]
    @test test_ht.g″ == [69.0, 9.0, 9.0, 69.0, 81.0, 103.0]

    # Not specifying ν_min
    test_ht = Absorption.read_hitran(CO2_test_file, mol=2, iso=1, ν_max=6400)
    @test length(test_ht.mol) == 9

    # Not specifying ν_max
    test_ht = Absorption.read_hitran(CO2_test_file, mol=2, iso=1, ν_min=6000)
    @test length(test_ht.mol) == 7

    # Not specifying ν_min OR ν_max
    test_ht = Absorption.read_hitran(CO2_test_file, mol=2, iso=1)
    @test length(test_ht.mol) == 12

end

# Test that absorption cross sections are calculated correctly
# (Not using pre-saved interpolator)

@testset "absorption_cross_section_hitran" begin

    println("Testing absorption_cross_section_hitran...")

    ####
    #### These tests check that for one molecule (CO2), over a temperature/
    #### pressure grid and in a specific band (6000<ν<6400), our Voigt
    #### broadening implementation closely matches the HAPI output under
    #### the same conditions
    ####

    # Temperature and pressure grids.
    # Note that these are pre-defined for testing -- the values are defined in generateHapiTests.py
    # To test against a different range, you must change the grids in that
    # file and rerun it. Then, you can change it here.

    pressures = 250:250:1250
    temperatures = 100:75:400

    # Get the test data
    CO2_file = artifact("CO2")
    test_ht = Absorption.read_hitran(CO2_file, mol=2, iso=1, ν_min=6000, ν_max=6400)

    grid = 6000:0.01:6400;

    # Threshold -- our value must be within ϵ of the HAPI value
    ϵ = 3.6e-27

    # Create a HitranModel
    model = make_hitran_model(test_ht, Voigt(), CEF=HumlicekWeidemann32SDErrorFunction())

    # Loop over every temperature/pressure combo and test that the results match HAPI
    @showprogress 1 "Testing HAPI equivalence (On CO2 Band)..." for temp in temperatures
        for pres in pressures
            jl_cs = absorption_cross_section(model, grid, pres, temp)
            py_cs = array_type(default_architecture)(readdlm("test_profiles/Voigt_CO2_T" * string(temp) * "_P" * string(pres) * ".csv"))
            Δcs = abs.(jl_cs - py_cs)
            @test maximum(Δcs) < ϵ
        end
    end

    ####
    #### Now test HAPI equivalence with other molecules
    ####

    names = ["H2O", "CO2", "O3", "N2O", "CO"]
    molecules = [1, 2, 3, 4, 5]
    isotopes = [1, 1, 1, 1, 1]

    # Loop over every temperature/pressure combo and test that the results match HAPI
    # (Doing this for other molecules)
    @showprogress 1 "Testing HAPI equivalence (On Other Molecules)..." for name in names

        pres = 1000
        temp = 250

        # Get the test data
        test_ht = Absorption.read_hitran(artifact(name), iso=1, ν_min=6000, ν_max=6400)
        # Create a HitranModel
        model = make_hitran_model(test_ht, Voigt(), CEF=HumlicekWeidemann32SDErrorFunction())

        jl_cs = absorption_cross_section(model, grid, pres, temp)
        py_cs = array_type(default_architecture)(readdlm("test_profiles/Voigt_" * name * "_T250_P1000.csv"))
        Δcs = abs.(jl_cs - py_cs)
        @test maximum(Δcs) < ϵ
    end
end

# Test that absorption cross sections if calculated using wavenumbers

@testset "absorption_cross_section_wavelengths" begin

    println("Testing absorption_cross_section_wavelengths...")

    hitran_data = read_hitran(artifact("CO2"), mol=2, iso=1, ν_min=6000, ν_max=6400)
    model = make_hitran_model(hitran_data, Voigt(), wing_cutoff = 40, CEF=HumlicekWeidemann32SDErrorFunction(), architecture=CPU())
    grid = 6000:0.01:6400
    @test absorption_cross_section(model, grid, 1000.1, 296.1) ≈ reverse(absorption_cross_section(model, reverse(1e7 ./ collect(grid)), 1000.1, 296.1, wavelength_flag=true));
end

# Test that absorption cross sections are calculated correctly
# using a new interpolator

@testset "absorption_cross_section_interpolator" begin

    println("Testing absorption_cross_section_interpolator...")

    # Get the test data
    test_ht = Absorption.read_hitran(artifact("CO2"), iso=1, ν_min=6000, ν_max=6400)

    # Pressure and temperature grids
    pressures = 250:250:1250
    temperatures = 100:75:400

    # Wavelength grid
    ν_grid = 6000:0.01:6400

    # Create the Interpolation Model
    interp_model = make_interpolation_model(test_ht, Voigt(), ν_grid, pressures,
                                            temperatures, CEF=HumlicekWeidemann32SDErrorFunction())

    # Threshold -- our value must be within ϵ of the HAPI value
    ϵ = 3.6e-27

    # Loop over every temperature/pressure combo and test that the results match HAPI
    for temp in temperatures
        for pres in pressures
            jl_cs = absorption_cross_section(interp_model, ν_grid, pres, temp)
            py_cs = readdlm("test_profiles/Voigt_CO2_T" * string(temp) * "_P" * string(pres) * ".csv")
            Δcs = abs.(jl_cs - py_cs)
            @test maximum(Δcs) < ϵ
        end
    end

end

# Test that checks whether the cross-section auto-differentiation works

@testset "absorption_cross_section_autodiff" begin

    println("Testing absorption_cross_section_autodiff...")

    # Load HITRAN data
    hitran_data = read_hitran(artifact("CO2"), mol=2, iso=1, ν_min=6000, ν_max=6400)

    # Create the model with parameters
    model = make_hitran_model(hitran_data, Voigt(), wing_cutoff = 40, CEF=HumlicekWeidemann32SDErrorFunction(), architecture=CPU())

    # Compute the cross-section with autodifferentiation
    value, derivs = absorption_cross_section(model, 6000:0.01:6400, 1000.1, 296.1, autodiff=true);

end


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

## getindex
const rep = 20
const reps = rep^2

function sparse_getindex_perf()
    ## create some sparse matrices (need to be square):
    seed = 1
    srand(seed)

    small = 10^3
    med = 10^4
    large = 10^5
    huge = 10^6
    # # 1 entry per line
    # ss = []
    # push!(ss, sprand(small, small, 1e-3))
    # #push!(ss, sprand(med, med, 1e-4))
    # push!(ss, sprand(large, large, 1e-5))

    # 10 entries per line
    ts = []
    push!(ts, sprand(small, small, 1e-2))
    #push!(ts, sprand(med, med, 1e-3))
    push!(ts, sprand(large, large, 1e-4))

    # 100 entries per line
    us = []
    push!(us, sprand(small, small, 1e-1))
    #push!(us, sprand(med, med, 1e-2))
    push!(us, sprand(large, large, 1e-3))
    #push!(us, sprand(huge, huge, 1e-4))

    # # 1000 entries per line
    # vs = []
    # push!(vs, sprand(small, small, 1.0))
    # #push!(vs, sprand(med, med, 1e-1))
    # push!(vs, sprand(large, large, 1e-2))
    # #push!(vs, sprand(huge, huge, 1e-3))

    ## using uint32 (works up to 10^9)
    uus = []
    for u in us
        push!(uus, SparseMatrixCSC(u.m, u.n, map(UInt32,u.colptr), map(UInt32,u.rowval), u.nzval))
    end

    # test performance with matrices in us, uus and ts for
    # - integer indexing
    # - range indexing
    # - ordered array indexing
    # - disordered array indexing
    # - 1d indexing with integers and booleans

    # setup:
    # - indices
    intinds = nothing
    logicalinds = nothing # needs to be generated for a specific matrix size.
    rangeinds = 121:237
    orderedinds = collect(rangeinds)
    disorderedinds = orderedinds[randperm(length(orderedinds))]

    inds = [(intinds, "integers"), (logicalinds, "logical array"), (rangeinds, "a range"),
            (orderedinds, "a ordered array"), (disorderedinds, "a disordered array")]

    # - matrix sizes
    sizes = [(1, "small", "Small sparse matrix"), (2, "medium", "Medium sparse matrix")]

    # - matrix types
    mattyp = [(ts, "10 entries/column"), (us, "100 entries/column"), (uus, "100 entries/column uint32")]

    # - functions
    funs = [(integer_indexing, 1, "indexing"), (one_arg_indexing, 1, "1d indexing"),
            (row_indexing, 2, "indexing rows"), (col_indexing, 2, "indexing columns"),
            (row_col_indexing, 3, "indexing rows & columns")]

    # performance tests:
    counters = [1,1] # for small and medium matrix
    # integer indexing
    for (sz,s1,s2) in sizes # size of the matrix
        for (mt, ms) in mattyp  # type of the matrix
            m = mt[sz]
            c = counters[sz]
            @timeit integer_indexing(m) "sparse_getindex_$s1$c" "$s2 with $ms, indexing with integers"
            counters[sz] += 1
        end
    end

    # range & array indexing
    for (sz,s1,s2) in sizes # size of the matrix
        for (mt, ms) in mattyp  # type of the matrix
            m = mt[sz]
            for (fun, nargs, funstr) in funs[3:5] # indexing test function
                for (ind,indstr) in inds[3:5] # type of indices
                    c = counters[sz]
                    if indstr=="logical array"
                        # make a logical array of the right size
                        ind = sprand(Bool, size(m,1)..., 1e-5)
                    end
                    if nargs==2
                        @timeit fun(m, ind)  "sparse_getindex_$s1$c" "Sparse matrix with $ms, $funstr with $indstr"
                    elseif nargs==3
                        @timeit fun(m, ind, ind) "sparse_getindex_$s1$c" "Sparse matrix with $ms, $funstr with $indstr"
                    else
                        error("Something is amiss here.")
                    end
                    counters[sz] += 1
                end
            end
        end
    end

    # linear indexing
    for (sz,s1,s2) in sizes # size of the matrix
        for (mt, ms) in mattyp  # type of the matrix
            m = mt[sz]
            for (ind,indstr) in inds[2:5] # type of indices
                if indstr=="logical array"
                    if sz==2
                        continue # logical indexing with medium size sparse matrix takes too long
                    end
                    # make a logical array of the right size
                    ind = sprand(Bool, size(m)..., 1e-5)
                    c = counters[sz]
                    @timeit one_arg_indexing(m, ind) "sparse_getindex_$s1$c" "$s2 with $ms, linear indexing with $indstr"
                    counters[sz] += 1
                else
                    c = counters[sz]
                    @timeit one_arg_indexing(m, ind) "sparse_getindex_$s1$c" "$s2 with $ms, linear indexing with $indstr"
                    counters[sz] += 1
                end
            end
        end
    end

end

function integer_indexing(A)
    # index with two random integers
    nI, nJ = size(A)
    rI = 1:nI
    rJ = 1:nJ
    tmp = zero(eltype(A))
    for i in rand(rI, reps)
        for j in rand(rJ, rep)
            tmp += A[i,j]
        end
    end
    tmp
end

function row_indexing(A, rowinds)
    # index rows with rowinds and columns with a random integer
    nI, nJ = size(A)
    rI = 1:nI
    rJ = 1:nJ
    tmp = zero(eltype(A))
    for j in rand(rJ, reps)
        tmp += sum(A[rowinds,j])
    end
    tmp
end

function col_indexing(A, colinds)
    # index rows with a random integer and columns with colinds
    nI, nJ = size(A)
    rI = 1:nI
    rJ = 1:nJ
    tmp = zero(eltype(A))
    for i in rand(rI, div(reps,10) )
        tmp += sum(A[i,colinds])
    end
    tmp
end

function row_col_indexing(A, rowinds, colinds)
    # index rows with rowinds and columns with colinds
    # we need:
    (maximum(rowinds)+rep < size(A,1) && maximum(colinds)+rep < size(A, 2)) || error("bad rowinds or colinds")
    nI, nJ = size(A)
    rI = 1:nI
    rJ = 1:nJ
    for i in 1:10
        for j in 1:10
            tmp2 = A[rowinds.+i, colinds.+j]
        end
    end
end

function one_arg_indexing(A, lininds)
    # This is for 1d-indexing and indexing with one array of logicals.
    # Both return a nx1 sparse matrix.
    tmp = zero(eltype(A))
    if isa(eltype(A), Bool)
        tmp = sum(A[lininds])
    else
        for i in 1:rep
            tmp += sum(A[lininds])
        end
    end
    tmp
end


using View5D, Test, JavaCall
# JavaCall.addOpts("-Djava.awt.headless=true") # This is needed for Linux and Mac, but the Applet.init() causes an exception
# JavaCall.init()

data1 = rand(5,5,3,2,2);
data2 = rand(5,5,3,2,2);
data3 = rand(5,5,3,4,2); # more elements

@testset "start viewers" begin
    v = @vv data1; # start the viewer
    @ve data2; # append along element
    @vt data3; # append along time
    @vt data3; # append along time
    @vr data3; # replace first
    w = @vv data2
    @vr v data3; # replace first
    @test 6 == get_num_times();
    @test 4 == get_num_elements();
    sleep(0.1); hide_viewer() ;
end

@testset "interaction with markers" begin
    @vv data1; # start the viewer
    markers = empty_marker_list(2,2)
    markers[1][3]=1.0
    markers[2][4]=2.0
    markers[2][5]=1.0
    markers[3][3]=3.0
    markers[3][5]=2.0
    markers[4][3]=1.5
    markers[4][4]=1.5
    markers[4][5]=2.0
    sleep(0.1); import_marker_lists(markers)
    sleep(0.1); exported = export_marker_lists()
    mydiff = exported .- markers
    for d in 1:4
        sleep(0.1);
        @test mydiff[d][2:9] == zeros(8)
    end
    sleep(0.1);
    @test nothing != export_markers_string()
    sleep(0.1);
    delete_all_marker_lists()
end

data1 = rand(5,5,3,1,1) .+ 1im.*rand(5,5,3,1,1);
data2 = rand(5,5,3,1,1) .+ 1im.*rand(5,5,3,1,1);
data3 = rand(5,5,3,2,1) .+ 1im.*rand(5,5,3,2,1); # more elements

@testset "complex-valued display" begin
    @test nothing != @vp data1; # start a new viewer in phase mode
    @test nothing == set_gamma(1.0);
    @test nothing != @vep data2; # start a new viewer in phase mode
    @vp data3; # new phase viewer
    @vrp data3; # replace phase data
end

@testset "view5d datatypes" begin
    @test nothing != view5d(rand(Float32,2,2,2,2,2)) # -> float
    @test nothing != view5d(rand(Float64,2,2,2,2,2)) # -> double
    @test nothing != view5d(rand(ComplexF32,2,2,2,2,2)) # > Complex
    @test nothing != view5d(rand(ComplexF64,2,2,2,2,2)) # -> Complex
    @test nothing != view5d(rand(Int8,2,2,2,2,2)) # -> Byte
    @test nothing != view5d(rand(UInt8,2,2,2,2,2)) # -> Byte
    @test nothing != view5d(rand(Int16,2,2,2,2,2)) # -> Short (signed)
    @test nothing != view5d(rand(UInt16,2,2,2,2,2)) # -> Unsigned Short
    @test nothing != view5d(rand(Int32,2,2,2,2,2)) # -> int
    @test nothing != view5d(rand(UInt32,2,2,2,2,2)) # -> double
    @test nothing != view5d(rand(Int64,2,2,2,2,2)) # -> int
    @test nothing != view5d(rand(UInt64,2,2,2,2,2)) # -> int
    @test nothing == close_all()
    v = view5d(rand(Float32,2,2,2,2,2))
    @test nothing == set_gamma(1.0,v, element=0)
    @test nothing == set_display_size(400,500,v)
    @test nothing == set_fontsize(25,v)
    @test nothing == hide_viewer(v)
    @test nothing == to_front(v)
    @test nothing == set_value_unit("mm", v,element=1)
    @test nothing == set_min_max_thresh(0.0, 3.0, v,element=1)
    @test nothing == process_key_main_window('C', v)
    @test nothing == process_key_element_window('C', v)
    @test nothing == update_panels()
    @test nothing == set_time(0,v)
    @test nothing == set_element(0,v)
    @test nothing == set_times_linked(false,v)
    @test nothing == set_elements_linked(true,v)
    @test nothing == update_panels()
    @test nothing == close_all()
end


module V3

using DataFrames
using DocStringExtensions
using Printf
using UUIDs
using ...STMMRunner
export run_mstm

abstract type MSTMOutput end

struct NearField
    spheres::DataFrame
    field::DataFrame
end

struct MSTMFixedOutput <: MSTMOutput
    q_ext_unpolarized::Float64
    q_abs_unpolarized::Float64
    q_scat_unpolarized::Float64
    asymmetric_parameter_unpolarized::Float64
    q_ext_parallel::Float64
    q_abs_parallel::Float64
    q_scat_parallel::Float64
    q_ext_perpendicular::Float64
    q_abs_perpendicular::Float64
    q_scat_perpendicular::Float64
    scattering_matrix::DataFrame
    near_field::Union{NearField,Nothing}
end

struct MSTMRandomOutput <: MSTMOutput
    q_ext::Float64
    q_abs::Float64
    q_scat::Float64
    asymmetric_parameter::Float64
    scattering_matrix::DataFrame
    scattering_matrix_expansion_coefficients::DataFrame
    t_matrix::Union{DataFrame,Nothing}
end

"""
Use the given configuration to run MSTM v3.

$(SIGNATURES)

- If `keep = true`, the working directory will not be removed after the run.
- `mstm_exe_name` specifies the name or path of your compiled MSTM v3 executable.
"""
function run_mstm(cfg::STMMConfig; keep::Bool = false, mstm_exe_name::String = "mstm3")
    current_dir = pwd()

    id = string(uuid1())
    if cfg.working_directory == ""
        dir = joinpath(pwd(), "mstm_tmp_$id")
    else
        dir = cfg.working_directory
    end
    results = nothing
    proc = nothing

    try
        if !ispath(dir)
            @debug "[Run MSTM] Creating workding directory"
            mkdir(dir)
        end

        @debug "[Run MSTM] Entering working directory"
        cd(dir)

        @debug "[Run MSTM] Writing MSTM input file"
        write_input(cfg)

        @debug "[Run MSTM] Running MSTM..."
        proc = open(
            `mpiexec -n $(cfg.number_processors) $(mstm_exe_name)`,
            stdout;
            write = true,
        )
        wait(proc)

        @debug "[Run MSTM] Collecting MSTM output"
        results = collect_output(cfg)
    catch e
        @error "[Run MSTM] $e"
        @error "[Run MSTM] Runtime error occurred"
    finally
        @debug "[Run MSTM] Leaving working directory"
        cd(current_dir)

        if !isnothing(proc)
            @debug "[Run MSTM] Killing MSTM process"
            kill(proc)
            sleep(0.2)
            kill(proc) # We need two SIGTERMs to actually kill the process.
            sleep(0.2)
            if process_running(proc)
                @warn "Failed to kill, please retry manually."
            end
        end

        if !keep && occursin(String(id), dir)
            @assert ispath(dir)
            @debug "[Run MSTM] Removing temporary directory"
            rm(dir; force = true, recursive = true)
        end

        return results
    end
end

function format_sphere(sphere::Sphere)::String
    radius_and_center = @sprintf "%.4e %.4e %.4e %.4e " sphere.r sphere.x sphere.y sphere.z

    if !isempty(sphere.t_matrix)
        return radius_and_center * sphere.t_matrix
    else
        return radius_and_center *
               (@sprintf "%.4e %.4e %.4e %.4e" sphere.m.re sphere.m.im sphere.β.re sphere.β.im)
    end
end

# We set `write_sphere_data` to `0` since we do not need them.
# We set `near_field_output_data` to `2` (the full set).
# The manual uses `near_field_translation_distance`, which should be `near_field_distance`.
function write_input(cfg::STMMConfig)
    inp = """
number_spheres
$(length(cfg.spheres))
output_file
$(cfg.output_file)
run_print_file
$(cfg.run_print_file)
write_sphere_data
0
medium_real_ref_index
$(cfg.layers[1].m.re)
medium_imag_ref_index
$(cfg.layers[1].m.im)
medium_real_chiral_factor
$(cfg.layers[1].β.re)
medium_imag_chiral_factor
$(cfg.layers[1].β.im)
target_euler_angles_deg
$(join(cfg.target_euler_angles_deg, ","))
mie_epsilon
$(cfg.mie_epsilon)
translation_epsilon
$(cfg.translation_epsilon)
solution_epsilon
$(cfg.solution_epsilon)
max_number_iterations
$(cfg.max_iterations)
store_translation_matrix
$(Int(cfg.store_translation_matrix))
sm_number_processors
$(cfg.sm_number_processors)
near_field_distance
$(cfg.near_field_translation_distance)
iterations_per_correction
$(cfg.iterations_per_correction)
min_scattering_angle_deg
$(cfg.θ_min)
max_scattering_angle_deg
$(cfg.θ_max)
min_scattering_plane_angle_deg
$(cfg.ϕ_min)
max_scattering_plane_angle_deg
$(cfg.ϕ_max)
delta_scattering_angle_deg
$(cfg.Δθ)
incident_or_target_frame
$(Int(cfg.frame))
normalize_scattering_matrix
$(Int(cfg.normalize_scattering_matrix))
azimuth_average_scattering_matrix
$(Int(cfg.azimuthal_average))
gaussian_beam_constant
$(cfg.gaussian_beam_constant)
gaussian_beam_focal_point
$(join(cfg.gaussian_beam_focal_point, ","))
fixed_or_random_orientation
$(Int(cfg.orientation))
incident_azimuth_angle_deg
$(cfg.α)
incident_polar_angle_deg
$(cfg.β)
calculate_scattering_coefficients
$(Int(cfg.calculate_scattering_coefficients))
scattering_coefficient_file
$(cfg.scattering_coefficient_file)
track_iterations
$(Int(cfg.track_iterations))
calculate_near_field
$(Int(cfg.calculate_near_field))
near_field_plane_coord
$(cfg.near_field_plane_coord)
near_field_plane_position
$(cfg.near_field_plane_position)
near_field_plane_vertices
$(join(cfg.near_field_plane_vertices, ","))
spacial_step_size
$(cfg.near_field_step_size)
polarization_angle_deg
$(cfg.polarization_angle_deg)
near_field_output_file
$(cfg.near_field_output_file)
near_field_output_data
2
plane_wave_epsilon
$(cfg.plane_wave_epsilon)
calculate_t_matrix
$(Int(cfg.calculate_t_matrix))
t_matrix_file
$(cfg.t_matrix_file)
t_matrix_convergence_epsilon
$(cfg.t_matrix_epsilon)
sphere_sizes_and_positions
$(join(map(format_sphere, cfg.spheres),"\n"))
end_of_options
"""

    open("mstm.inp", "w") do io
        write(io, inp)
    end
end

function read_ints(s::AbstractString)::Vector{Int}
    return map(x -> parse(Int, x), filter(!isempty, split(s, r"\s+")))
end

function read_floats(s::AbstractString)::Vector{Float64}
    return map(x -> parse(Float64, x), filter(!isempty, split(s, r"\s+")))
end

function collect_output(cfg::STMMConfig)::MSTMOutput
    @assert isfile(cfg.output_file)

    out = split(read(open(cfg.output_file), String), "\n")
    i = 1

    if cfg.orientation == FixedOrientation
        q = zeros(10)
    else
        q = zeros(4)
    end

    θ = Float64[]
    ϕ = Float64[]
    s11 = Float64[]
    s12 = Float64[]
    s13 = Float64[]
    s14 = Float64[]
    s21 = Float64[]
    s22 = Float64[]
    s23 = Float64[]
    s24 = Float64[]
    s31 = Float64[]
    s32 = Float64[]
    s33 = Float64[]
    s34 = Float64[]
    s41 = Float64[]
    s42 = Float64[]
    s43 = Float64[]
    s44 = Float64[]

    while !occursin("calculation results for run", out[i])
        i += 1
    end

    # Skip header
    i += 2

    if cfg.orientation == FixedOrientation
        # Fixed orientation

        # Read unpolarized efficiencies
        q[1:4] = read_floats(out[i+1])
        i += 2

        # Read parallel efficiencies
        q[5:7] = read_floats(out[i+1])
        i += 2

        # Read perpendicular efficiencies
        q[8:10] = read_floats(out[i+1])
        i += 4

        # Read scattering matrix
        has_phi = !cfg.azimuthal_average
        while !isempty(out[i])
            v = read_floats(out[i])
            i += 1

            push!(θ, v[1])
            push!(ϕ, has_phi ? v[2] : 0.0)

            push!(s44, v[end])
            push!(s43, v[end-1])
            push!(s42, v[end-2])
            push!(s41, v[end-3])
            push!(s34, v[end-4])
            push!(s33, v[end-5])
            push!(s32, v[end-6])
            push!(s31, v[end-7])
            push!(s24, v[end-8])
            push!(s23, v[end-9])
            push!(s22, v[end-10])
            push!(s21, v[end-11])
            push!(s14, v[end-12])
            push!(s13, v[end-13])
            push!(s12, v[end-14])
            push!(s11, v[end-15])
        end
        scattering_matrix = DataFrame(;
            θ,
            ϕ,
            s11,
            s12,
            s13,
            s14,
            s21,
            s22,
            s23,
            s24,
            s31,
            s32,
            s33,
            s34,
            s41,
            s42,
            s43,
            s44,
        )

        if cfg.calculate_near_field && isfile(cfg.near_field_output_file)
            nf = split(read(open(cfg.near_field_output_file), String), "\n")

            # Read near field grid size
            Nx, Ny = read_ints(nf[1])
            Ns = read_ints(nf[2])[1]

            sx = Float64[]
            sy = Float64[]
            sz = fill(cfg.near_field_plane_position, Ns)
            r = Float64[]
            for j = 1:Ns
                v = read_floats(nf[j+2])
                push!(sx, v[1])
                push!(sy, v[2])
                push!(r, v[3])
            end

            if cfg.near_field_plane_coord == 1
                spheres = DataFrame(; x = sz, y = sx, z = sy, r)
            elseif cfg.near_field_plane_coord == 2
                spheres = DataFrame(; x = sy, y = sz, z = sx, r)
            else
                @assert cfg.near_field_plane_coord == 3
                spheres = DataFrame(; x = sx, y = sy, z = sz, r)
            end

            # Read near field data
            E² = Float64[]
            Ex = ComplexF64[]
            Ey = ComplexF64[]
            Ez = ComplexF64[]
            Hx = ComplexF64[]
            Hy = ComplexF64[]
            Hz = ComplexF64[]
            X = Float64[]
            Y = Float64[]
            Z = fill(cfg.near_field_plane_position, Nx * Ny)
            for j = 1:Nx*Ny
                v = read_floats(nf[j+2+Ns])

                push!(X, v[1])
                push!(Y, v[2])

                push!(Ex, complex(v[3], v[4]))
                push!(Ey, complex(v[5], v[6]))
                push!(Ez, complex(v[7], v[8]))

                push!(Hx, complex(v[9], v[10]))
                push!(Hy, complex(v[11], v[12]))
                push!(Hz, complex(v[13], v[14]))
            end

            if cfg.near_field_plane_coord == 1
                x, y, z = Z, X, Y
            elseif cfg.near_field_plane_coord == 2
                x, y, z = Y, Z, X
            else
                @assert cfg.near_field_plane_coord == 3
                x, y, z = X, Y, Z
            end

            field = DataFrame(; x, y, z, Ex, Ey, Ez, Hx, Hy, Hz)
            near_field = NearField(spheres, field)
        else
            near_field = nothing
        end

        return MSTMFixedOutput(q..., scattering_matrix, near_field)
    else
        # Random orientation

        # Read efficiencies
        q[1:4] = read_floats(out[i+1])
        i += 3

        # Read scattering matrix
        Nθ = Int(round((cfg.θ_max - cfg.θ_min) / cfg.Δθ)) + 1
        for j = 1:Nθ
            v = read_floats(out[i+j])

            push!(θ, v[1])
            push!(ϕ, 0.0)

            # Refer to Mishchenko and Yurkin (2017) for the reciprocity relation
            push!(s11, v[2])
            push!(s12, v[3])
            push!(s13, v[4])
            push!(s14, v[5])
            push!(s21, v[3])
            push!(s22, v[6])
            push!(s23, v[7])
            push!(s24, v[8])
            push!(s31, -v[4])
            push!(s32, -v[7])
            push!(s33, v[9])
            push!(s34, v[10])
            push!(s41, v[5])
            push!(s42, v[8])
            push!(s43, -v[10])
            push!(s44, v[11])
        end
        scattering_matrix = DataFrame(;
            θ,
            ϕ,
            s11,
            s12,
            s13,
            s14,
            s21,
            s22,
            s23,
            s24,
            s31,
            s32,
            s33,
            s34,
            s41,
            s42,
            s43,
            s44,
        )
        i += Nθ + 3

        # Read scattering matrix expansion coefficients
        w = Int[]
        a11 = Float64[]
        a12 = Float64[]
        a13 = Float64[]
        a14 = Float64[]
        a22 = Float64[]
        a23 = Float64[]
        a24 = Float64[]
        a32 = Float64[]
        a33 = Float64[]
        a34 = Float64[]
        a44 = Float64[]

        while !isempty(out[i])
            v = read_floats(out[i])
            i += 1

            push!(w, Int(v[1]))
            push!(a11, v[2])
            push!(a22, v[3])
            push!(a33, v[4])
            push!(a23, v[5])
            push!(a32, v[6])
            push!(a44, v[7])
            push!(a12, v[8])
            push!(a34, v[9])
            push!(a13, v[10])
            push!(a24, v[11])
            push!(a14, v[12])
        end
        scattering_matrix_expansion_coefficients =
            DataFrame(; w, a11, a12, a13, a14, a22, a23, a24, a32, a33, a34, a44)

        return MSTMRandomOutput(
            q...,
            scattering_matrix,
            scattering_matrix_expansion_coefficients,
            nothing,
        )
    end
end

end


export
    parent_ranges,
    get_range,
    set_range!,
    set_ranges!,
    expander_range

import Scruff: get_range

#####################################################################
#                                                                   #
# Algorithm building blocks for setting and expanding the ranges of #
# all the current instances in a runtime.                           #
#                                                                   #
# order is a topological sort of the network in the runtime.        #
# size is a uniform target size for each instance.                  #
#                                                                   #
#####################################################################

"""
    parent_ranges(runtime::Runtime, var::Variable{I,J,O}, depth = typemax(Int)) where {I,J,O}

Returns the ranges of the parents of the given variable.

See [`Scruff.get_range`](@ref)
"""
function parent_ranges(runtime::Runtime, var::Variable{I,J,O}, depth = typemax(Int)) where {I,J,O}
    net = get_network(runtime)
    pars = get_parents(net, var)
    result = []
    for p in pars
        pinst = current_instance(runtime, p)
        push!(result, get_range(runtime, pinst, depth))
    end
    T = isempty(result) ? Vector{O} : typejoin([typeof(x) for x in result]...)
    return convert(Vector{T}, result)
end

"""
    get_range(runtime::DynamicRuntime, v::Variable{I,J,O}, depth = 1) where {I,J,O}

Returns the range of the most recent instance of the given variable.

See [`Scruff.get_range`](@ref)
"""
function get_range(runtime::DynamicRuntime, v::Variable{I,J,O}, depth = 1) where {I,J,O}
    inst = current_instance(runtime, v)
    range = get_range(runtime, inst, depth)
    if range !== nothing
        return range
    elseif has_previous_instance(runtime, v)
        prev = previous_instance(runtime, v)
        if has_range(runtime, prev)
            return get_range(runtime, prev, depth)
        else
            return O[]
        end
    else
        return O[]
    end
end

"""
    get_range(runtime::InstantRuntime, v::Node{O}, depth = 1) where O

Returns the range of the given node.

See [`Scruff.get_range`](@ref)
"""
function get_range(runtime::InstantRuntime, v::Node{O}, depth = 1) where O
    inst = runtime.instances[v]
    range = get_range(runtime, inst, depth)
    if range !== nothing
        return range
    else
        return O[]
    end
end

"""
    set_ranges!(runtime::InstantRuntime, evidence = Dict{Symbol, Score}(),
        size = 10, depth = 1,
        order = topsort(get_initial_graph(get_network(runtime))),
        placeholder_beliefs = get_placeholder_beliefs(runtime))

Set the ranges of all current instances in the runtime.

This method first checks whether ranges exist for the runtime at the desired depth,
with the desired range size, and with the same evidence. If so, it doesn't do anything.
If the depth is less than 1, it doesn't do anything.
Otherwise, it uses the support operator to compute ranges of variables in order.
Placeholders should have ranges set already in `placeholder_beliefs`.
For expanders, it recursively sets the ranges of the subnetworks at depth - 1.

Returns a flag indicating whether any instance has a changed range.
"""
function set_ranges!(runtime::InstantRuntime,
    evidence::Dict{Symbol,Score} = Dict{Symbol, Score}(),
    size :: Int = 10, depth :: Int = 1,
    order = topsort(get_initial_graph(get_network(runtime))),
    placeholder_beliefs = get_placeholder_beliefs(runtime))
    if depth < 1
        return false
    end
    nodes = get_nodes(get_network(runtime))
    # If we require greater depth or greater size or different evidence, we have to do it again
    if has_state(runtime, :nodes) && has_state(runtime, :range_size) && has_state(runtime, :range_depth) && has_state(runtime, :range_evidence) &&
        get_state(runtime, :range_size) >= size  && get_state(runtime, :range_depth) >= depth && get_state(runtime, :range_evidence) == evidence &&
        get_state(runtime, :nodes) == nodes
        return false
    else
        set_state!(runtime, :range_size, size)
        set_state!(runtime, :range_depth, depth)
        set_state!(runtime, :range_evidence, evidence)
        set_state!(runtime, :nodes, nodes)
    end
    changed = false
    for v in order
        O = output_type(v)
        rng::Vector{O} = O[]
        curr = get_range(runtime, v, depth)
        inst = current_instance(runtime, v)
        if v isa Placeholder
            pi = placeholder_beliefs[v.name]
            rng = support(pi, (), size, output_type(v)[])
            chng = rng != curr
        else
            sf = get_sfunc(inst)
            if length(curr) < size
                parranges = parent_ranges(runtime, v, depth)
                if isa(sf, Expander)
                    rc = expander_range(runtime, v, size, depth)
                    rng= rc[1]
                    chng = rc[2]
                else
                    rng = support(sf, tuple(parranges...), size, curr)
                    chng = rng != curr
                end
                if v.name in keys(evidence)
                    ev = evidence[v.name]
                    if ev isa HardScore
                        if !(ev.value in rng)
                            push!(rng, ev.value)
                            chng = true
                        end
                    elseif ev isa LogScore
                        for k in keys(ev.logscores)
                            if !(k in rng)
                                push!(rng, k)
                                chng = true
                            end
                        end
                    end
                end
                changed = changed || chng
            else
                rng = curr
            end
        end
        set_range!(runtime, inst, rng, depth)
    end
    if changed
        delete_state!(runtime, :solution)
    end
    return changed
end

"""
    function expander_range(runtime :: Runtime, v :: Variable,
        target_size :: Int, depth :: Int)

    Recursively compute the range of the expander and subnetworks up to the given depth.

Computing the range of the expander expands enough of the parent
range to reach the desired target size, or expands all the parents fully.
"""
function expander_range(runtime :: Runtime, v :: Variable,
                        target_size :: Int, depth :: Int)
    changed = false
    net = runtime.network
    parents = get_parents(net, v)
    parent = parents[1]
    parrange = get_range(runtime, parent, depth)
    rangeset = Set()
    subranges = Dict()
    for p in parrange
        if expanded(runtime, v, p)
            subnet = expansion(runtime, v, p)
            subrun = subruntime(runtime, v, subnet)
            subout = current_instance(subrun, get_node(subnet, :out))
            if has_range(subrun, subout, depth - 1)
                subrange = get_range(subrun, subout, depth - 1)
                subranges[p] = subrange
                union!(rangeset, subrange)
            else
                subranges[p] = []
            end
        else
            subranges[p] = []
        end
    end
    todo = Dict()
    for p in parrange
        todo[p] = length(subranges[p])
    end
    while length(rangeset) < target_size && !isempty(todo)
        p = argmin(todo)
        delete!(todo, p)
        (subnet, chng) = expand!(runtime, v, p)
        changed = chng
        subrun = subruntime(runtime, v, subnet)
        if depth > 1
            order = topsort(get_initial_graph(subnet))
            outvar = get_node(subrun, :out)
            # Cannot have evidence on subnet
            if set_ranges!(subrun, Dict{Symbol, Score}(), target_size - length(rangeset), depth - 1, order)
                changed = true
            end
            inst = current_instance(subrun, outvar)
            rng = get_range(subrun, inst, depth - 1)
            union!(rangeset, rng)
        end
    end
    result = output_type(v)[]
    for x in rangeset
        push!(result, x)
    end
    sort!(result)
    return (result, changed)
end


include("preamble.jl")

using InteractiveUtils: code_llvm, code_warntype
using Statistics: mean

using Transducers: Reduction, start, __foldl__, __simple_foldl__,
    maybe_usesimd, SideEffect, _prepare_map, _map!

"""
    llvm_ir(f, args) :: String

Get LLVM IR of `f(args...)` as a string.
"""
llvm_ir(f, args) = sprint(code_llvm, f, Base.typesof(args...))

"""
    julia_ir(f, args) :: String

Get Julia IR of `f(args...)` as a string.
"""
julia_ir(f, args) = sprint(code_warntype, f, Base.typesof(args...))

matchedlines(r, s) = [m.match for m in eachmatch(r, s)]
anyunions(s) = matchedlines(r".*UNION.*", s)
nmatches(r, s) = count(_ -> true, eachmatch(r, s))

@testset "map!" begin
    xf = Filter(x -> -0.5 < x < 0.5) |> Map(x -> 2x)
    xs = Float64[]
    ys = Float64[]

    @testset "history" begin
        # This test used to work but it wouldn't work now (unless the
        # compiler becomes _extremely_ smart).
        ir = llvm_ir(map!, (xf, ys, xs))
        @test_broken nmatches(r"fmul <4 x double>", ir) >= 4
        @test_broken nmatches(r"fcmp [a-z]* <4 x double>", ir) >= 4
    end

    @testset for simd in [false, true, :ivdep]
        args = _prepare_map(xf, ys, xs, simd)
        ir = llvm_ir(_map!, args)
        @test nmatches(r"fmul <4 x double>", ir) >= 4
        @test nmatches(r"fcmp [a-z]* <4 x double>", ir) >= 4
    end
end

@testset "Cat SIMD" begin
    coll = [Float64[]]
    rf = maybe_usesimd(Reduction(Cat(), +), true)
    ir = llvm_ir(transduce, (rf, 0.0, coll))
    @test_broken_if(
        VERSION < v"1.1-",
        nmatches(r"fadd (fast )?<4 x double>", ir) >= 9)
end

unsafe_setter(ys) =
    function((i, x),)
        @inbounds ys[i] = x
        return
    end

@testset "foreach SIMD" begin
    xf_double = Map(x -> 2x)

    params = [
        :Enumerate => xf_double |> Enumerate(),
        :TeeZip => xf_double |> Transducers.TeeZip(Count()) |> Map(reverse),
        #= Zip was working before...
        :Zip => Zip(Count(), xf_double),
        =#
    ]

    @testset "$key" for key in first.(params)
        xf = Dict(params)[key]

        xs = ones(10)
        ys = zero(xs)

        foreach(unsafe_setter(ys), xf, xs)
        @test ys == 2xs

        # Manually "expand" `foreach` internal (so that I can observe
        # SIMD in the IR).
        rf = Reduction(xf, SideEffect(unsafe_setter(ys)))
        rf = maybe_usesimd(rf, true)
        fill!(ys, 0)
        transduce(rf, nothing, xs)
        @test ys == 2xs

        ir = llvm_ir(transduce, (rf, nothing, xs))
        @test nmatches(r"fmul <4 x double>", ir) >= 4
    end
end

@testset "PartitionBy" begin
    xf = PartitionBy(x -> x > 0) |>
        Filter(xs -> mean(abs, xs) < 1.0) |>
        Map(prod)

    # Union coming from
    okunion = r"UNION\{NOTHING, *TUPLE\{INT64, *INT64\}\}"

    coll = Float64[]
    rf = Reduction(xf, +)
    val = start(rf, 0.0)
    ir = julia_ir(__foldl__, (rf, val, coll))
    @test anyunions(replace(ir, okunion => "")) == []

    # If Julia becomes clever enough to make `__simple_foldl__`
    # type-stable, there is no need to maintain current complex code:
    simple_ir = julia_ir(__simple_foldl__, (rf, val, coll))
    @test !isempty(anyunions(replace(simple_ir, okunion => "")))
end


################################################################################
#
#  Field access
#
################################################################################

function elem_type(::Type{RelOrdQuoRing{T1, T2, T3}}) where { T1, T2, T3 }
  S = elem_type(T1)
  return RelOrdQuoRingElem{T1, T2, T3, S}
end

function elem_type(::RelOrdQuoRing{T1, T2, T3}) where { T1, T2, T3 }
  S = elem_type(T1)
  return RelOrdQuoRingElem{T1, T2, T3, S}
end

base_ring(Q::RelOrdQuoRing) = Q.base_ring

ideal(Q::RelOrdQuoRing) = Q.ideal

basis_pmatrix(Q::RelOrdQuoRing) = Q.basis_pmatrix

parent(x::RelOrdQuoRingElem) = x.parent

parent_type(::Type{RelOrdQuoRingElem{T1, T2, T3, S}}) where { T1, T2, T3, S } = RelOrdQuoRing{T1, T2, T3}

################################################################################
#
#  Hashing
#
################################################################################

hash(x::RelOrdQuoRingElem, h::UInt) = hash(x.elem, h)

################################################################################
#
#  Copying
#
################################################################################

Base.deepcopy_internal(x::RelOrdQuoRingElem, dict::IdDict) =
        RelOrdQuoRingElem(parent(x), Base.deepcopy_internal(x.elem, dict))

################################################################################
#
#  I/O
#
################################################################################

function show(io::IO, Q::RelOrdQuoRing)
  print(io, "Quotient of $(base_ring(Q))")
end

function show(io::IO, x::RelOrdQuoRingElem)
  print(io, "$(x.elem)")
end

################################################################################
#
#  Parent object overloading
#
################################################################################

function (Q::RelOrdQuoRing{T1, T2, T3})(x::RelOrdQuoRingElem{T1, T2, T3, S}) where { T1, T2, T3, S }
  parent(x) !== Q && error("Cannot coerce element into quotient ring")
  return x
end

function (Q::RelOrdQuoRing{T1, T2, T3})(x::S) where { T1, T2, T3, S }
  parent(x) !== base_ring(Q) && error("Cannot coerce element into the quotient ring")
  return RelOrdQuoRingElem(Q, x)
end

function (Q::RelOrdQuoRing)(x::Integer)
  return Q(base_ring(Q)(x))
end

function (Q::RelOrdQuoRing)(x::fmpz)
  return Q(base_ring(Q)(x))
end

################################################################################
#
#  Quotient function
#
################################################################################

function quo(O::Union{NfRelOrd, AlgAssRelOrd}, I::Union{NfRelOrdIdl, AlgAssRelOrdIdl})
  @assert order(I) === O
  # We should check that I is not zero
  Q = RelOrdQuoRing(O, I)
  f = RelOrdQuoMap(O, Q)
  return Q, f
end

Nemo.ResidueRing(O::Union{NfRelOrd, AlgAssRelOrd}, I::Union{NfRelOrdIdl, AlgAssRelOrdIdl}) = RelOrdQuoRing(O, I)

################################################################################
#
#  Parent check
#
################################################################################

function check_parent(x::RelOrdQuoRingElem, y::RelOrdQuoRingElem)
  if parent(x) !== parent(y)
    error("Elements must have same parents")
  end
  return true
end

################################################################################
#
#  Arithmetic
#
################################################################################

function +(x::RelOrdQuoRingElem, y::RelOrdQuoRingElem)
  check_parent(x, y)
  return parent(x)(x.elem + y.elem)
end

function -(x::RelOrdQuoRingElem, y::RelOrdQuoRingElem)
  check_parent(x, y)
  return parent(x)(x.elem - y.elem)
end

function -(x::RelOrdQuoRingElem)
  return parent(x)(-x.elem)
end

function *(x::RelOrdQuoRingElem, y::RelOrdQuoRingElem)
  check_parent(x, y)
  return parent(x)(x.elem * y.elem)
end

function mul!(z::RelOrdQuoRingElem, x::RelOrdQuoRingElem, y::RelOrdQuoRingElem)
  z.elem = mul!(z.elem, x.elem, y.elem)
  z.elem = mod!(z.elem, parent(z))
  return z
end

function add!(z::RelOrdQuoRingElem, x::RelOrdQuoRingElem, y::RelOrdQuoRingElem)
  z.elem = add!(z.elem, x.elem, y.elem)
  z.elem = mod!(z.elem, parent(z))
  return z
end

function *(x::Integer, y::RelOrdQuoRingElem)
  return parent(y)(x * y.elem)
end

*(x::RelOrdQuoRingElem, y::Integer) = y*x

function *(x::fmpz, y::RelOrdQuoRingElem)
  return parent(y)(x * y.elem)
end

*(x::RelOrdQuoRingElem, y::fmpz) = y*x

function ^(a::RelOrdQuoRingElem, f::fmpz)
  if fits(Int, f)
    return a^Int(f)
  end
  f == 0 && return one(parent(a))
  f == 1 && return deepcopy(a)
  if f < 0
    f = -f
    a = inv(a)
  end
  b = a^(div(f, 2))
  b *= b
  if isodd(f)
    b *= a
  end
  return b
end

function ^(a::RelOrdQuoRingElem, b::Int)
  if b == 0
    return one(parent(a))
  elseif b == 1
    return deepcopy(a)
  else
    if b < 0
      a = inv(a)
      b = -b
    end
    bit = ~((~UInt(0)) >> 1)
    while (UInt(bit) & b) == 0
      bit >>= 1
    end
    z = deepcopy(a)
    bit >>= 1
    while bit != 0
      z = mul!(z, z, z)
      if (UInt(bit) & b) != 0
        z = mul!(z, z, a)
      end
      bit >>= 1
    end
    return z
  end
end

################################################################################
#
#  Special elements
#
################################################################################

iszero(x::RelOrdQuoRingElem) = iszero(x.elem)

isone(x::RelOrdQuoRingElem) = isone(x.elem)

function one(Q::RelOrdQuoRing)
  return Q(one(base_ring(Q)))
end

function zero(Q::RelOrdQuoRing)
  return Q(zero(base_ring(Q)))
end

################################################################################
#
#  Equality
#
################################################################################

function ==(x::RelOrdQuoRing, y::RelOrdQuoRing)
  return base_ring(x) === base_ring(y) && ideal(x) == ideal(y)
end

==(x::RelOrdQuoRingElem, y::RelOrdQuoRingElem) = x.elem == y.elem


################################################################################
#
#   GENERATE THE CODE THAT A UNITCELL FILE IN THIS LIBRARY NEEDS
#
################################################################################

################################################################################
#
#   VERSION CODE (ONLY SINGLE IMPLEMENTATION)
#
################################################################################

# code for a specific unitcell implementation
function getCodeUnitcellImplementation(
            unitcell    :: U,
            name        :: String = "myunitcell",
            implementation  :: Int64  = 1;
            labeltype_site  :: DataType = Nothing,
            labeltype_bond  :: DataType = Nothing
        ) :: String where {D,LS,LB,N, S<:AbstractSite{LS,D}, B<:AbstractBond{LB,N}, U<:AbstractUnitcell{S,B}}

    # set the correct label type for sites and bonds
    sitelabeltype = (labeltype_site == Nothing ? LS : labeltype_site) :: DataType
    bondlabeltype = (labeltype_bond == Nothing ? LB : labeltype_bond) :: DataType

    # build the string of lattice vectors
    lattice_vector_string = ""
    for l in latticeVectors(unitcell)
        lattice_vector_string = lattice_vector_string * "            Float64" * string(l) * ",\n"
    end
    if length(lattice_vector_string) > 2
        lattice_vector_string = lattice_vector_string[1:end-2] * "\n"
    end

    # build a string for all sites
    site_string = ""
    for s in sites(unitcell)
        if sitelabeltype <: AbstractString
            site_string = site_string * "            newSite(S, Float64" * string(point(s)) * ", LS(\"" * string(label(s)) *  "\")),\n"
        else
            site_string = site_string * "            newSite(S, Float64" * string(point(s)) * ", LS(" * string(label(s)) *  ")),\n"
        end
    end
    if length(site_string) > 2
        site_string = site_string[1:end-2] * "\n"
    end

    # build a string for all bonds
    bond_string = ""
    for b in bonds(unitcell)
        if bondlabeltype <: AbstractString #newBond(B, 1,1, LB("1"), (+1,0))
            bond_string = bond_string * "            newBond(B, " *
                string(from(b)) * ", " * string(to(b)) *
                ", LB(\"" * string(label(b)) *  "\"), " * string(wrap(b)) * "),\n"
        else
            bond_string = bond_string * "            newBond(B, " *
                string(from(b)) * ", " * string(to(b)) *
                ", LB(" * string(label(b)) *  "), " * string(wrap(b)) * "),\n"
        end
    end
    if length(bond_string) > 2
        bond_string = bond_string[1:end-2] * "\n"
    end

    # generate the complete generating code as a string
    generating_code = "" *
        "# Implementation\n" *
        "# - implementation " * string(implementation) * "\n" *
        "# - labels <: " * string(sitelabeltype) * " / " * string(bondlabeltype) * "\n" *
        "function getUnitcell" * uppercase(name[1]) * name[2:end] * "(\n" *
        "            unitcell_type  :: Type{U},\n" *
        "            implementation :: Val{" * string(implementation) * "}\n" *
        "        ) :: U where {LS<:" *
        string(sitelabeltype) * ",LB<:" * string(bondlabeltype) *
        ",S<:AbstractSite{LS," * string(D) * "},B<:AbstractBond{LB," * string(N) * "}, " *
        "U<:AbstractUnitcell{S,B}}\n" *
        "    \n" *
        "    # return a new Unitcell\n" *
        "    return newUnitcell(\n" *
        "        # Type of the unitcell\n" *
        "        U,\n" *
        "        # Bravais lattice vectors\n" *
        "        Vector{Float64}[\n" *
        lattice_vector_string *
        "        ],\n" *
        "        # Sites\n" *
        "        S[\n" *
        site_string *
        "        ],\n" *
        "        # Bonds\n" *
        "        B[\n" *
        bond_string *
        "        ]\n" *
        "    )\n" *
        "end"

    # print the generating code
    return generating_code
end

# code for a specific unitcell implementation but simplified
function getCodeUnitcellImplementationSimplified(
            unitcell    :: U,
            name        :: String = "myunitcell",
            implementation     :: Int64  = 1;
            labeltype_site  :: DataType = Nothing,
            labeltype_bond  :: DataType = Nothing
        ) :: String where {D,LS,LB,N, S<:AbstractSite{LS,D}, B<:AbstractBond{LB,N}, U<:AbstractUnitcell{S,B}}

    # set the correct label type for sites and bonds
    sitelabeltype = (labeltype_site == Nothing ? LS : labeltype_site) :: DataType
    bondlabeltype = (labeltype_bond == Nothing ? LB : labeltype_bond) :: DataType

    # build the string of lattice vectors
    lattice_vector_string = ""
    for l in latticeVectors(unitcell)
        lattice_vector_string = lattice_vector_string * "            Float64" * string(l) * ",\n"
    end
    if length(lattice_vector_string) > 2
        lattice_vector_string = lattice_vector_string[1:end-2] * "\n"
    end

    # build a string for all sites
    site_string = ""
    for (i,s) in enumerate(sites(unitcell))
        site_string = site_string * "            newSite(S, Float64" * string(point(s)) * ", getDefaultLabel(LS)),\n"
    end
    if length(site_string) > 2
        site_string = site_string[1:end-2] * "\n"
    end

    # build a string for all bonds
    bond_string = ""
    for b in bonds(unitcell) #newBond(B, 1,1, LB("1"), (+1,0))
        bond_string = bond_string * "            newBond(B, " *
            string(from(b)) * ", " * string(to(b)) *
            ", getDefaultLabel(LB), " * string(wrap(b)) * "),\n"
    end
    if length(bond_string) > 2
        bond_string = bond_string[1:end-2] * "\n"
    end

    # generate the complete generating code as a string
    generating_code = "" *
        "# Implementation\n" *
        "# - implementation " * string(implementation) * "\n" *
        "# - labels <: " * string(sitelabeltype) * " / " * string(bondlabeltype) * "\n" *
        "function getUnitcell" * uppercase(name[1]) * name[2:end] * "(\n" *
        "            unitcell_type  :: Type{U},\n" *
        "            implementation :: Val{" * string(implementation) * "}\n" *
        "        ) :: U where {LS<:" *
        string(sitelabeltype) * ",LB<:" * string(bondlabeltype) *
        ",S<:AbstractSite{LS," * string(D) * "},B<:AbstractBond{LB," * string(N) * "}, " *
        "U<:AbstractUnitcell{S,B}}\n" *
        "    \n" *
        "    # return a new Unitcell\n" *
        "    return newUnitcell(\n" *
        "        # Type of the unitcell\n" *
        "        U,\n" *
        "        # Bravais lattice vectors\n" *
        "        Vector{Float64}[\n" *
        lattice_vector_string *
        "        ],\n" *
        "        # Sites\n" *
        "        S[\n" *
        site_string *
        "        ],\n" *
        "        # Bonds\n" *
        "        B[\n" *
        bond_string *
        "        ]\n" *
        "    )\n" *
        "end"

    # print the generating code
    return generating_code
end

# code for unitcell templates
function getCodeUnitcellImplementationTemplate() :: String

    # build the string of lattice vectors
    lattice_vector_string = ""
    lattice_vector_string = lattice_vector_string * "            <A1>,\n"
    lattice_vector_string = lattice_vector_string * "            <A2>,\n"
    lattice_vector_string = lattice_vector_string * "            ...\n"

    # build a string for all sites
    site_string = ""
    site_string = site_string * "            newSite(S, <POSITION>, LS(<LABEL>)),\n"
    site_string = site_string * "            newSite(S, <POSITION>, LS(<LABEL>)),\n"
    site_string = site_string * "            ...\n"

    # build a string for all bonds
    bond_string = ""
    bond_string = bond_string * "            newBond(B, <FROM>, <TO>, LB(<LABEL>), <WRAP>),\n"
    bond_string = bond_string * "            newBond(B, <FROM>, <TO>, LB(<LABEL>), <WRAP>),\n"
    bond_string = bond_string * "            ...\n"

    # generate the complete generating code as a string
    generating_code = "" *
        "# Implementation\n" *
        "# - implementation <VERSION>\n" *
        "# - labels <: <SITELABELTYPE> / <BONDLABELTYPE>\n" *
        "function getUnitcell<UNITCELLNAME>(\n" *
        "            unitcell_type  :: Type{U},\n" *
        "            implementation :: Val{<VERSION>}\n" *
        "        ) :: U where {LS<:<SITELABELTYPE>,LB<:<BONDLABELTYPE>, " *
        "S<:AbstractSite{LS,<SPACEDIMENSION>},B<:AbstractBond{LB,<LATTICEDIMENSION>}, " *
        "U<:AbstractUnitcell{S,B}}\n" *
        "    \n" *
        "    # return a new Unitcell\n" *
        "    return newUnitcell(\n" *
        "        # Type of the unitcell\n" *
        "        U,\n" *
        "        # Bravais lattice vectors\n" *
        "        Vector{Float64}[\n" *
        lattice_vector_string *
        "        ],\n" *
        "        # Sites\n" *
        "        S[\n" *
        site_string *
        "        ],\n" *
        "        # Bonds\n" *
        "        B[\n" *
        bond_string *
        "        ]\n" *
        "    )\n" *
        "end"

    # print the generating code
    return generating_code
end

# code for unitcell implementation fallback
function getCodeUnitcellImplementationFallback(
            unitcell    :: U,
            name        :: String = "myunitcell",
            implementation     :: Int64  = 1
        ) :: String where {D,LS,LB,N, S<:AbstractSite{LS,D}, B<:AbstractBond{LB,N}, U<:AbstractUnitcell{S,B}}

    # generate the complete generating code as a string
    generating_code = "" *
        "# Implementation\n" *
        "# - implementation " * string(implementation) * "\n" *
        "# - labels <: Any\n" *
        "# --> FALLBACK (raises error)\n" *
        "function getUnitcell" * uppercase(name[1]) * name[2:end] * "(\n" *
        "            unitcell_type  :: Type{U},\n" *
        "            implementation :: Val{" * string(implementation) * "}\n" *
        "        ) :: U where {LS,LB,S<:AbstractSite{LS," * string(D) * "},B<:AbstractBond{LB," * string(N) * "}, " *
        "U<:AbstractUnitcell{S,B}}\n" *
        "    \n" *
        "    # error since this implementation has no implementation yet\n" *
        "    error(\"Implementation " * string(implementation) * " of " * name * " unitcell has no implementation for label types \" * string(LS) * \" / \" * string(LB) * \" yet\")\n" *
        "end"

    # print the generating code
    return generating_code
end

# export all different ways of generating unitcell implementation code
export getCodeUnitcellImplementation, getCodeUnitcellImplementationSimplified,
    getCodeUnitcellImplementationTemplate, getCodeUnitcellImplementationFallback

################################################################################
#
#   CODE FOR THE FILE STRUCTURE
#
################################################################################

# get the code for Unitcell file header
function getCodeUnitcellFileHeader(
            name :: String,
            D, N
        )

    # compile the string
    complete_code = ""

    # add the main comment to the top
    complete_code *= "################################################################################\n"
    complete_code *= "#\n"
    complete_code *= "#   " * uppercase(name) * " LATTICE\n"
    complete_code *= "#\n"
    complete_code *= "################################################################################\n\n\n\n"

    # FIRST big headline (fallback / reference)
    complete_code *= "# REFERENCE / FALLBACK (for generic AbstractUnitcell type)\n\n"

    # referencing function for abstract type
    complete_code *= "# Referencing to individual functions by wrapping in Val()\n"
    complete_code *= "function getUnitcell" * uppercase(name[1]) * name[2:end] * "(\n"
    complete_code *= "            unitcell_type  :: Type{U},\n"
    complete_code *= "            implementation :: Int64 = 1\n"
    complete_code *= "        ) :: U where {LS,LB,S<:AbstractSite{LS," * string(D) * "},B<:AbstractBond{LB," * string(N) * "},U<:AbstractUnitcell{S,B}}\n"
    complete_code *= "    \n"
    complete_code *= "    # call the respective subfunction by converting to val type\n"
    complete_code *= "    return getUnitcell" * uppercase(name[1]) * name[2:end] * "(unitcell_type, Val(implementation))\n"
    complete_code *= "end\n\n"

    # fallback function for abstract type
    complete_code *= "# Fallback for all implementations (if Val{V} is not found)\n"
    complete_code *= "function getUnitcell" * uppercase(name[1]) * name[2:end] * "(\n"
    complete_code *= "            unitcell_type  :: Type{U},\n"
    complete_code *= "            implementation :: Val{V}\n"
    complete_code *= "        ) :: U where {LS,LB,S<:AbstractSite{LS," * string(D) * "},B<:AbstractBond{LB," * string(N) * "},U<:AbstractUnitcell{S,B},V}\n"
    complete_code *= "    \n"
    complete_code *= "    # fallback / fail due to missing implementation\n"
    complete_code *= "    error(\"Implementation \" * string(V) * \" of " * name * " unitcell (label types \" * string(LS) * \" / \" * string(LB) * \") not implemented yet\")\n"
    complete_code *= "end\n\n\n\n"

    # SECOND big headline (wrapper for concrete type)
    complete_code *= "# WRAPPER FUNCTIONS (for concrete Unitcell type) call general function\n\n"

    # wrapper with only implementation (DEFAULT)
    complete_code *= "# wrapper function for passing no label types (and implementation) (DEFAULT)\n"
    complete_code *= "function getUnitcell" * uppercase(name[1]) * name[2:end] * "(\n"
    complete_code *= "            implementation :: Int64 = 1\n"
    complete_code *= "        ) :: Unitcell{Site{Int64," * string(D) * "},Bond{Int64," * string(N) * "}}\n"
    complete_code *= "    \n"
    complete_code *= "    # create a suitable unitcell of the Unitcell type\n"
    complete_code *= "    return getUnitcell" * uppercase(name[1]) * name[2:end] * "(Unitcell{Site{Int64," * string(D) * "},Bond{Int64," * string(N) * "}}, implementation)\n"
    complete_code *= "end\n\n"

    # wrapper with common label type and implementation
    complete_code *= "# wrapper function for passing common label type (and implementation)\n"
    complete_code *= "function getUnitcell" * uppercase(name[1]) * name[2:end] * "(\n"
    complete_code *= "            label_type     :: Type{L},\n"
    complete_code *= "            implementation :: Int64 = 1\n"
    complete_code *= "        ) :: Unitcell{Site{L," * string(D) * "},Bond{L," * string(N) * "}} where L\n"
    complete_code *= "    \n"
    complete_code *= "    # create a suitable unitcell of the Unitcell type\n"
    complete_code *= "    return getUnitcell" * uppercase(name[1]) * name[2:end] * "(Unitcell{Site{L," * string(D) * "},Bond{L," * string(N) * "}}, implementation)\n"
    complete_code *= "end\n\n"

    # wrapper with common label type and implementation
    complete_code *= "# wrapper function for passing site / bond label types (and implementation)\n"
    complete_code *= "function getUnitcell" * uppercase(name[1]) * name[2:end] * "(\n"
    complete_code *= "            label_type_site :: Type{LS},\n"
    complete_code *= "            label_type_bond :: Type{LB},\n"
    complete_code *= "            implementation  :: Int64 = 1\n"
    complete_code *= "        ) :: Unitcell{Site{LS," * string(D) * "},Bond{LB," * string(N) * "}} where {LS,LB}\n"
    complete_code *= "    \n"
    complete_code *= "    # create a suitable unitcell of the Unitcell type\n"
    complete_code *= "    return getUnitcell" * uppercase(name[1]) * name[2:end] * "(Unitcell{Site{LS," * string(D) * "},Bond{LB," * string(N) * "}}, implementation)\n"
    complete_code *= "end\n\n\n\n"

    # add an export for the relevant function
    complete_code *= "export getUnitcell" * uppercase(name[1]) * name[2:end] * "\n\n\n\n"

    # add the distinction to implementation code
    complete_code *= "################################################################################\n"
    complete_code *= "#\n"
    complete_code *= "#   VERSION IMPLEMENTATIONS FROM HERE ON\n"
    complete_code *= "#\n"
    complete_code *= "################################################################################"

    # return the code
    return complete_code
end

# export header code function
export getCodeUnitcellFileHeader

################################################################################
#
#   PRINTING CODE INTO THE TERMINAL / OTHER IO
#
################################################################################

# print the unitcell implementation code
function printCodeUnitcellImplementation(
            io          :: IO,
            unitcell    :: U,
            name        :: String = "myunitcell",
            implementation     :: Int64  = 1;
            labeltype_site  :: DataType = Nothing,
            labeltype_bond  :: DataType = Nothing
        ) where {D,LS,LB,N, S<:AbstractSite{LS,D}, B<:AbstractBond{LB,N}, U<:AbstractUnitcell{S,B}}

    # get the code
    code = getCodeUnitcellImplementation(unitcell, name, implementation, labeltype_site=labeltype_site, labeltype_bond=labeltype_bond)

    # print the code
    print(io, code)
end
function printCodeUnitcellImplementation(
            unitcell    :: U,
            name        :: String = "myunitcell",
            implementation     :: Int64  = 1;
            labeltype_site  :: DataType = Nothing,
            labeltype_bond  :: DataType = Nothing
        ) where {D,LS,LB,N, S<:AbstractSite{LS,D}, B<:AbstractBond{LB,N}, U<:AbstractUnitcell{S,B}}

    # get the code
    code = getCodeUnitcellImplementation(unitcell, name, implementation, labeltype_site=labeltype_site, labeltype_bond=labeltype_bond)

    # print the code
    print(code)
end

# export print functions
export printCodeUnitcellImplementation

################################################################################
#
#   WRITE A COMPLETE UNITCELL CODE TO FILE
#
################################################################################

# write a complete file for a unitcell
function writeUnitcellFile(
            unitcell    :: U,
            uc_name     :: String = "myunitcell",
            folder      :: String = "./",
            implementation     :: Int64  = 1
            ;
            generate_fallback :: Bool = true,
            generate_default  :: Bool = true
        ) where {D,LS,LB,N, S<:AbstractSite{LS,D}, B<:AbstractBond{LB,N}, U<:AbstractUnitcell{S,B}}

    # get the name for the function
    name = filter(c -> isletter(c), uc_name)
    # get the filename
    filename = folder * (folder[end] == "/" ? "" : "/") * lowercase(filter(c -> isletter(c) || c=='-', uc_name)) * ".jl"

    # mode for file opening
    openmode = "a"

    # compile the string
    complete_code = ""

    # check if the file already exists (then, leave out all header stuff)
    if !isfile(filename)

        # other mode of opening the file
        openmode = "w"

        # add the main comment to the top
        complete_code *= getCodeUnitcellFileHeader(name, D, N)

        # add space
        complete_code *= "\n\n\n"

        # DEFAULT VERSION

        # check if defaults should be generated
        if generate_default
            # check if default needs a fallback
            if generate_fallback
                # add the fallback for implementation 1 to the code
                complete_code *= getCodeUnitcellImplementationFallback(unitcell, name, 1) * "\n\n"
            end
            # generate a default implementation for Number
            complete_code *= getCodeUnitcellImplementationSimplified(unitcell, name, 1, labeltype_bond=Number, labeltype_site=Number) * "\n\n"
            # generate a default implementation for AbstractString
            complete_code *= getCodeUnitcellImplementationSimplified(unitcell, name, 1, labeltype_bond=AbstractString, labeltype_site=AbstractString) * "\n\n"
        end

    end

    # CUSTOM VERSION

    # maybe some space
    if implementation != 1
        complete_code *= "\n\n"
    end

    # check if custom implementation needs a fallback
    if generate_fallback && implementation != 1
        # add the fallback for implementation 1 to the code
        complete_code *= getCodeUnitcellImplementationFallback(unitcell, name, implementation) * "\n\n"
    end
    # generate a normal implementation code
    complete_code *= getCodeUnitcellImplementation(unitcell, name, implementation) * "\n\n"

    # open the respective file and write the code into that file
    f = open(filename, openmode)
    write(f, complete_code)
    close(f)

end

# export the function
export writeUnitcellFile