"""
`module  JAC.Hfs`
    ... a submodel of JAC that contains all methods for computing HFS A and B coefficients, hyperfine sublevel representations, etc.
"""
module Hfs

    using Printf, ..AngularMomentum, ..Basics,  ..Defaults, ..InteractionStrength, ..ManyElectron, ..Radial, ..Nuclear, ..TableStrings

    """
    `struct  Hfs.InteractionMatrix`  ... defines a type for storing the T^1 and T^2 interaction matrices for a given basis.

        + calcT1   ::Bool               ... true, if the matrixT1 has been calculated and false otherwise.
        + calcT2   ::Bool               ... true, if the matrixT2 has been calculated and false otherwise.
        + matrixT1 ::Array{Float64,2}   ... T1 interaction matrix
        + matrixT2 ::Array{Float64,2}   ... T2 interaction matrix

    """
    struct InteractionMatrix
        calcT1     ::Bool
        calcT2     ::Bool
        matrixT1   ::Array{Float64,2}
        matrixT2   ::Array
    end

    """
    `Hfs.InteractionMatrix()`  ... constructor for an `empty` instance of InteractionMatrix.
    """
    function InteractionMatrix()
        InteractionMatrix(false, false, zeros(2,2), zeros(2,2))
    end

    # `Base.show(io::IO, im::Hfs.InteractionMatrix)`  ... prepares a proper printout of the variable InteractionMatrix.
    function Base.show(io::IO, im::Hfs.InteractionMatrix)
        println(io, "calcT1:           $(im.calcT1)  ")
        println(io, "calcT1:           $(im.calcT1)  ")
        println(io, "matrixT1:         $(im.matrixT1)  ")
        println(io, "matrixT2:         $(im.matrixT2)  ")
    end

    """
    `struct  Hfs.IJF_Vector`  ... defines a type for a IJF-coupled basis vector, here based on an ASF.

        + I        ::AngularJ64   ... Nuclear spin I.
        + F        ::AngularJ64   ... Total angular momentum F
        + levelJ   ::Level        ... Atomic level with well-defined total (electronic) angular momentum J

    """
    struct IJF_Vector
        I          ::AngularJ64
        F          ::AngularJ64
        levelJ     ::Level
    end

    """
    `Hfs.IJF_Vector()`  ... constructor for an `empty` instance of IJF_Vector.
    """
    function IJF_Vector()
        IJF_Vector(AngularJ64(0), AngularJ64(0), Level())
    end

    # `Base.show(io::IO, ijfVector::Hfs.IJF_Vector)`  ... prepares a proper printout of the variable ijfVector.
    function Base.show(io::IO, ijfVector::Hfs.IJF_Vector)
        println(io, "I:           $(ijfVector.I)  ")
        println(io, "F:           $(ijfVector.F)  ")
        println(io, "levelJ:      $(ijfVector.levelJ)  ")
    end

   """
    `struct  Hfs.IJF_Basis`  ... defines a type for a IJF-coupled basis.

        + vectors   ::Array{IJF_Vector,1}   ... List of IJF_Vectors that form the basis.
        + basisJ    ::Basis                 ... Electronic basis that allows access to the electronic orbitals and CSF basis.
    """
    struct IJF_Basis
        vectors     ::Array{IJF_Vector,1}
        basisJ      ::Basis
    end

    """
   `Hfs.IJF_Basis()`  ... constructor for an `empty` instance of IJF_Basis.
    """
    function IJF_Basis()
        IJF_Basis(IJF_Vector[], Basis())
    end

    # `Base.show(io::IO, ijfBasis::Hfs.IJF_Basis)`  ... prepares a proper printout of the variable ijfBasis.
    function Base.show(io::IO, ijfBasis::Hfs.IJF_Basis)
        println(io, "levelFs:           $(ijfBasis.levelFs)  ")
        println(io, "basisJ:            $(ijfBasis.basisJ)  ")
    end

    """
    `struct  Hfs.IJF_Level`  ... defines a type for a IJF-coupled level.

        + I              ::AngularJ64       ... Nuclear spin I.
        + F              ::AngularJ64       ... Total angular momentum F.
        + M              ::AngularM64       ... Total projection M, only defined if a particular sublevel is referred to.
        + parity         ::Parity           ... Parity of the level which corresponds to the electronic system.
        + energy         ::Float64          ... energy
        + hasStateRep    ::Bool             ... Determines whether this IJF-level 'point' to a physical representation of
                                                an state (i.e. an basis and corresponding mixing coefficient vector), or
                                                just to empty instances, otherwise.
        + basis          ::IJF_Basis        ... basis for this level
        + mc             ::Vector{Float64}  ... Vector of mixing coefficients w.r.t basis.
    """
    struct IJF_Level
        I                ::AngularJ64
        F                ::AngularJ64
        M                ::AngularM64
        parity           ::Parity
        energy           ::Float64
        hasStateRep      ::Bool
        basis            ::IJF_Basis
        mc               ::Vector{Float64}
    end

    """
    `Hfs.IJF_Level()`  ... constructor for an `empty` instance of IJF_Level.
    """
    function IJF_Level()
        IJF_Level(AngularJ64(0), AngularJ64(0), AngularJ64(0), AngularM64(0), Basics.plus, 0., false, IJF_Basis(), Float64[])
    end

    # `Base.show(io::IO, ijfLevel::Hfs.IJF_Level)`  ... prepares a proper printout of the variable ijfLevel::Hfs.IJF_Level.
    function Base.show(io::IO, ijfLevel::Hfs.IJF_Level)
        println(io, "I:              $(ijfLevel.I)  ")
        println(io, "J:              $(ijfLevel.J)  ")
        println(io, "F:              $(ijfLevel.F)  ")
        println(io, "M:              $(ijfLevel.M)  ")
        println(io, "parity:         $(ijfLevel.parity)  ")
        println(io, "energy:         $(ijfLevel.energy)  ")
        println(io, "hasStateRep:    $(ijfLevel.hasStateRep)  ")
        println(io, "basis:          $(ijfLevel.basis)  ")
        println(io, "mc:             $(ijfLevel.mc)  ")
    end

    """
    `struct  Hfs.IJF_Multiplet`  ... defines a type for a multiplet of IJF-coupled levels.

        + name     ::String                ... A name associated to the multiplet.
        + levelFs  ::Array{IJF_Level,1}    ... List of IJF-coupled levels.

    """
    struct IJF_Multiplet
        name       ::String
        levelFs    ::Array{IJF_Level,1}
    end

    """
    `Hfs.IJF_Multiplet()`  ... constructor for an `empty` instance of Hfs.IJF_Multiplet.
    """
    function IJF_Multiplet()
        IJF_Multiplet("", IJF_Level[])
    end

    # `Base.show(io::IO, ijfMultiplet::Hfs.IJF_Multiplet)`  ... prepares a proper printout of the variable ijfMultiplet::Hfs.IJF_Multiplet.
    function Base.show(io::IO, ijfMultiplet::Hfs.IJF_Multiplet)
        println(io, "name:           $(ijfMultiplet.name)  ")
        println(io, "levelFs:        $(ijfMultiplet.levelFs)  ")
    end

    """
    `struct  Hfs.Outcome`
        ... defines a type to keep the outcome of a HFS computation, such as the HFS A and B coefficients as well
            other results.

        + Jlevel                    ::Level            ... Atomic level to which the outcome refers to.
        + AIoverMu                  ::Float64          ... HFS A * I / mu value.
        + BoverQ                    ::Float64          ... HFS B / Q value
        + amplitudeT1               ::Complex{Float64} ... T1 amplitude
        + amplitudeT2               ::Complex{Float64} ... T2 amplitude
        + nuclearI                  ::AngularJ64       ... nuclear spin
        + hasFlevels                ::Bool             ... True if the F-values and F-energies are specified explicitly.
        + Flevels                   ::Bool             ... Array to keep F-values and F-energies.
    """
    struct Outcome
        Jlevel                      ::Level
        AIoverMu                    ::Float64
        BoverQ                      ::Float64
        amplitudeT1                 ::Complex{Float64}
        amplitudeT2                 ::Complex{Float64}
        nuclearI                    ::AngularJ64
        hasFlevels                  ::Bool
        Flevels                     ::Bool
    end

    """
    `Hfs.Outcome()`  ... constructor for an `empty` instance of Hfs.Outcome for the computation of HFS properties.
    """
    function Outcome()
        Outcome(Level(), 0., 0., 0., 0., AngularJ64(0), false, false)
    end

    # `Base.show(io::IO, outcome::Hfs.Outcome)`  ... prepares a proper printout of the variable outcome::Hfs.Outcome.
    function Base.show(io::IO, outcome::Hfs.Outcome)
        println(io, "Jlevel:                    $(outcome.Jlevel)  ")
        println(io, "AIoverMu:                  $(outcome.AIoverMu)  ")
        println(io, "BoverQ:                    $(outcome.BoverQ)  ")
        println(io, "amplitudeT1:               $(outcome.amplitudeT1)  ")
        println(io, "amplitudeT2:               $(outcome.amplitudeT2)  ")
        println(io, "nuclearI:                  $(outcome.nuclearI)  ")
        println(io, "hasFlevels:                $(outcome.hasFlevels)  ")
        println(io, "Flevels:                   $(outcome.Flevels)  ")
    end

    """
    `struct  Settings`  ... defines a type for the details and parameters of computing HFS A and B coefficients.

        + calcT1                    ::Bool             ... True if T1-amplitudes (HFS A values) need to be calculated, and false otherwise.
        + calcT2                    ::Bool             ... True if T2-amplitudes (HFS B values) need to be calculated, and false otherwise.
        + calcNondiagonal           ::Bool             ... True if also (non-)diagonal hyperfine amplitudes are to be calculated and
                                                           printed, and false otherwise.
        + calcIJFexpansion          ::Bool             ... True if the selected atomic levels are to be represented in a IJF-coupled basis,
                                                           and false otherwise.
        + printBefore    ::Bool                        ... True if a list of selected levels is printed before the actual computations start.
        + printDeltaEF              ::Bool             ... True if the energy shift of E_F (with regard to E_J) is to be printed, and false otherwise.
        + levelSelection            ::LevelSelection   ... Specifies the selected levels, if any.
    """
    struct Settings
        calcT1                      ::Bool
        calcT2                      ::Bool
        calcNondiagonal             ::Bool
        calcIJFexpansion            ::Bool
        printBefore                 ::Bool
        printDeltaEF                ::Bool
        levelSelection              ::LevelSelection
    end

    """
    `Hfs.Settings(; calcT1::Bool=true,` calcT2::Bool=false, calcNondiagonal::Bool=false, calcIJFexpansion::Bool=false,
                        printBefore::Bool=false, printDeltaEF::Bool=false,
                        levelSelection::LevelSelection=LevelSelection())
        ... keyword constructor to overwrite selected value of Einstein line computations.
    """
    function Settings(; calcT1::Bool=true, calcT2::Bool=false, calcNondiagonal::Bool=false, calcIJFexpansion::Bool=false,
                        printBefore::Bool=false, printDeltaEF::Bool=false,
                        levelSelection::LevelSelection=LevelSelection())
        Settings(calcT1, calcT2, calcNondiagonal, calcIJFexpansion, printBefore, printDeltaEF, levelSelection)
    end

    # `Base.show(io::IO, settings::Hfs.Settings)`  ... prepares a proper printout of the variable settings::Hfs.Settings.
    function Base.show(io::IO, settings::Hfs.Settings)
        println(io, "calcT1:                   $(settings.calcT1)  ")
        println(io, "calcT2:                   $(settings.calcT2)  ")
        println(io, "calcNondiagonal:          $(settings.calcNondiagonal)  ")
        println(io, "calcIJFexpansion:         $(settings.calcIJFexpansion)  ")
        println(io, "printBefore:              $(settings.printBefore)  ")
        println(io, "printDeltaEF:             $(settings.printDeltaEF)  ")
        println(io, "levelSelection:           $(settings.levelSelection)  ")
    end

    """
    `Hfs.amplitude(kind::String, rLevel::Level, sLevel::Level, grid::Radial.Grid; printout::Bool=true)`
        ... to compute either the  T^(1) or T^(2) hyperfine amplitude <alpha_r J_r || T^(n)) || alpha_s J_s>
            for a given pair of levels. A value::ComplexF64 is returned.
    """
    function  amplitude(kind::String, rLevel::Level, sLevel::Level, grid::Radial.Grid; printout::Bool=true)
        #
        if  rLevel.parity != sLevel.parity   return( ComplexF64(0.) )   end
        nr = length(rLevel.basis.csfs);    ns = length(sLevel.basis.csfs);    matrix = zeros(ComplexF64, nr, ns)
        if  printout   printstyled("Compute hyperfine $(kind[1:5]) matrix of dimension $nr x $ns ... \n", color=:light_green)   end
        #
        for  r = 1:nr
            for  s = 1:ns
                me = 0.
                if  rLevel.basis.csfs[r].parity  != rLevel.parity    ||  sLevel.basis.csfs[s].parity  != sLevel.parity  ||
                    rLevel.parity != sLevel.parity    continue
                end
                #
                if      kind == "T^(1) amplitude"
                #--------------------------------
                    wa = compute("angular coefficients: 1-p, Grasp92", 0, 1, rLevel.basis.csfs[r], sLevel.basis.csfs[s])
                    for  coeff in wa
                        ja = Basics.subshell_2j(rLevel.basis.orbitals[coeff.a].subshell)
                        jb = Basics.subshell_2j(sLevel.basis.orbitals[coeff.b].subshell)
                        tamp  = InteractionStrength.hfs_t1(rLevel.basis.orbitals[coeff.a], sLevel.basis.orbitals[coeff.b], grid)
                        #
                        ## println("**  <$(coeff.a) || t1 || $(coeff.b)>  = $(coeff.T * tamp)   = $(coeff.T) * $tamp" )
                        me = me + coeff.T * tamp
                    end
                #
                elseif  kind == "T^(2) amplitude"
                #--------------------------------
                    wa = compute("angular coefficients: 1-p, Grasp92", 0, 2, rLevel.basis.csfs[r], sLevel.basis.csfs[s])
                    for  coeff in wa
                        ja = Basics.subshell_2j(rLevel.basis.orbitals[coeff.a].subshell)
                        jb = Basics.subshell_2j(sLevel.basis.orbitals[coeff.b].subshell)
                        tamp  = InteractionStrength.hfs_t2(rLevel.basis.orbitals[coeff.a], sLevel.basis.orbitals[coeff.b], grid)
                        #
                        ## println("**  <$(coeff.a) || t2 || $(coeff.b)>  = $(coeff.T * tamp)   = $(coeff.T) * $tamp" )
                        me = me + coeff.T * tamp
                    end
                #
                else    error("stop a")
                end
                #
                matrix[r,s] = me
                println("**                                                                    " *
                        "                      <$r || $kind || $s> = $me" )
            end
        end
        if  printout   printstyled("done.\n", color=:light_green)   end
        amplitude = transpose(rLevel.mc) * matrix * sLevel.mc
        #
        return( amplitude )
    end

    """
    `Hfs.computeAmplitudesProperties(outcome::Hfs.Outcome, grid::Radial.Grid, settings::Hfs.Settings, im::Hfs.InteractionMatrix)
        ... to compute all amplitudes and properties of for a given level; an outcome::Hfs.Outcome is returned for which the
            amplitudes and properties are now evaluated explicitly.
    """
    function  computeAmplitudesProperties(outcome::Hfs.Outcome, grid::Radial.Grid, settings::Hfs.Settings, im::Hfs.InteractionMatrix)
        AIoverMu = 0.0;   BoverQ = 0.0;   amplitudeT1 = 0.0;   amplitudeT2 = 0.0;    J = AngularMomentum.oneJ(outcome.Jlevel.J)
        if  settings.calcT1  &&  outcome.Jlevel.J != AngularJ64(0)
            if  im.calcT1   amplitudeT1 = transpose(outcome.Jlevel.mc) * im.matrixT1 * outcome.Jlevel.mc
            else            amplitudeT1 = Hfs.amplitude("T^(1) amplitude", outcome.Jlevel, outcome.Jlevel, grid)
            end

            AIoverMu = amplitudeT1 / sqrt(J * (J+1))
        end
        #
        if  settings.calcT2  &&  outcome.Jlevel.J != AngularJ64(0)
            if  im.calcT2   amplitudeT2 = transpose(outcome.Jlevel.mc) * im.matrixT2 * outcome.Jlevel.mc
            else            amplitudeT2 = Hfs.amplitude("T^(2) amplitude", outcome.Jlevel, outcome.Jlevel, grid)
            end

            BoverQ   = 2 * amplitudeT2 * sqrt( (2J-1) / ((J+1)*(2J+3)) )  ## * sqrt(J)
        end
        newOutcome = Hfs.Outcome( outcome.Jlevel, AIoverMu, BoverQ, amplitudeT1, amplitudeT2,
                                  outcome.nuclearI, outcome.hasFlevels, outcome.Flevels)
        return( newOutcome )
    end

    """
    `Hfs.computeHyperfineMultiplet(multiplet::Multiplet, nm::Nuclear.Model, grid::Radial.Grid, settings::Hfs.Settings; output=true)`
        ... to compute a hyperfine multiplet, i.e. a representation of hyperfine levels within a hyperfine-coupled basis as defined by the
            given (electronic) multiplet; a hfsMultiplet::IJF_Multiplet is returned.
    """
    function computeHyperfineMultiplet(multiplet::Multiplet, nm::Nuclear.Model, grid::Radial.Grid, settings::Hfs.Settings; output=true)
        println("")
        printstyled("Hfs.computeHyperfineMultiplet(): The computation of the hyperfine multiplet starts now ... \n", color=:light_green)
        printstyled("------------------------------------------------------------------------------------------ \n", color=:light_green)
        #
        hfsBasis     = Hfs.defineHyperfineBasis(multiplet, nm, settings)
        hfsMultiplet = Hfs.computeHyperfineRepresentation(hfsBasis, nm, grid, settings)
        # Print all results to screen
        hfsMultiplet = Basics.sortByEnergy(hfsMultiplet)
        Basics.tabulate("multiplet: energies",                                      hfsMultiplet)
        Basics.tabulate("multiplet: energy of each level relative to lowest level", hfsMultiplet)
        printSummary, iostream = Defaults.getDefaults("summary flag/stream")
        if  printSummary
            Basics.tabulate("multiplet: energies",                                      hfsMultiplet, stream=iostream)
            Basics.tabulate("multiplet: energy of each level relative to lowest level", hfsMultiplet, stream=iostream)
        end
        #
        if    output    return( hfsMultiplet )
        else            return( nothing )
        end
    end

    """
    `Hfs.computeHyperfineRepresentation(hfsBasis::IJF_Basis, nm::Nuclear.Model, grid::Radial.Grid, settings::Hfs.Settings)`
        ... to set-up and diagonalized the Hamiltonian matrix of H^(DFB) + H^(hfs) within the atomic hyperfine (IJF-coupled) basis;
            a hfsMultiplet::IJF_Multiplet is returned.
    """
    function computeHyperfineRepresentation(hfsBasis::IJF_Basis, nm::Nuclear.Model, grid::Radial.Grid, settings::Hfs.Settings)
        n = length(hfsBasis.vectors);   matrix = zeros(n,n)
        for  r = 1:n
            for  s = 1:n
                matrix[r,s] = 0.
                if  r == s    matrix[r,s] = matrix[r,s] + hfsBasis.vectors[r].levelJ.energy                  end
                if  hfsBasis.vectors[r].F              !=  hfsBasis.vectors[s].F                continue     end
                if  hfsBasis.vectors[r].levelJ.parity  !=  hfsBasis.vectors[s].levelJ.parity    continue     end
                # Now add the hyperfine matrix element for K = 1 (magnetic) and K = 2 (electric) contributions
                spinI = nm.spinI;   Jr = hfsBasis.vectors[r].levelJ.J;   Js = hfsBasis.vectors[s].levelJ.J;   F = hfsBasis.vectors[r].F
                Ix    = AngularMomentum.oneJ(spinI)
                #
                wa = AngularMomentum.phaseFactor([spinI, +1, Jr, +1, F])  *
                     AngularMomentum.Wigner_6j(spinI, Jr, F, Js, spinI, AngularJ64(1))
                wb = Hfs.amplitude("T^(1) amplitude", hfsBasis.vectors[r].levelJ, hfsBasis.vectors[s].levelJ, grid::Radial.Grid)
                wc = nm.mu * sqrt( (Ix+1)/Ix )
                matrix[r,s] = matrix[r,s] + wa * wb * wc     * 1.0e-6 ## fudge-factor to keep HFS interaction small
                #
                if  spinI  in [AngularJ64(0), AngularJ64(1//2)]                                 continue     end
                wa = AngularMomentum.phaseFactor([spinI, +1, Jr, +1, F]) *
                     AngularMomentum.Wigner_6j(spinI, Jr, F, Js, spinI, AngularJ64(2))
                wb = Hfs.amplitude("T^(2) amplitude", hfsBasis.vectors[r].levelJ, hfsBasis.vectors[s].levelJ, grid::Radial.Grid)
                wc = nm.Q / 2. * sqrt( (Ix+1)*(2Ix+3)/ (Ix*(2Ix-1)) )
                matrix[r,s] = matrix[r,s] + wa * wb * wc     * 1.0e-6 ## fudge-factor to keep HFS interaction small
            end
        end
        #
        # Diagonalize the matrix and set-up the representation
        eigen    = Basics.diagonalize("matrix: LinearAlgebra", matrix)
        levelFs  = Hfs.IJF_Level[]
        for  ev = 1:length(eigen.values)
            # Construct the eigenvector with regard to the given basis (not w.r.t the symmetry block)
            evector   = eigen.vectors[ev];    en = eigen.values[ev]
            parity    = Basics.plus;    F = AngularJ64(0);     MF = AngularM64(0)
            for  r = 1:length(hfsBasis.vectors)
                if  abs(evector[r]) > 1.0e-6
                    parity = hfsBasis.vectors[r].levelJ.parity
                    F      = hfsBasis.vectors[r].F;     MF = AngularM64(hfsBasis.vectors[r].F);   break    end
            end
            newlevelF = Hfs.IJF_Level(nm.spinI, F, MF, parity, en, true, hfsBasis, evector)
            push!( levelFs, newlevelF)
        end
        hfsMultiplet = Hfs.IJF_Multiplet("hyperfine", levelFs)

        return( hfsMultiplet )
    end

    """
    `Hfs.computeInteractionMatrix(basis::Basis, grid::Radial.Grid, settings::Hfs.Settings)`
        ... to compute the T^1 and/or T^2 interaction matrices for the given basis, i.e. (<csf_r || T^(n)) || csf_s>).
            An im::Hfs.InteractionMatrix is returned.
    """
    function  computeInteractionMatrix(basis::Basis, grid::Radial.Grid, settings::Hfs.Settings)
        #
        ncsf = length(basis.csfs);    matrixT1 = zeros(ncsf,ncsf);    matrixT2 = zeros(ncsf,ncsf)
        #
        if  settings.calcT1
            calcT1 = true;    matrixT1 = zeros(ncsf,ncsf)
            for  r = 1:ncsf
                for  s = 1:ncsf
                    if  basis.csfs[r].parity  != basis.csfs[s].parity   continue    end
                    #
                    wa = compute("angular coefficients: 1-p, Grasp92", 0, 1, basis.csfs[r], basis.csfs[s])
                    for  coeff in wa
                        ja   = Basics.subshell_2j(basis.orbitals[coeff.a].subshell)
                        jb   = Basics.subshell_2j(basis.orbitals[coeff.b].subshell)
                        tamp = InteractionStrength.hfs_t1(basis.orbitals[coeff.a], basis.orbitals[coeff.b], grid)
                        matrixT1[r,s] = matrixT1[r,s] + coeff.T * tamp
                    end
                end
            end
        else
            calcT1 = false;    matrixT1 = zeros(2,2)
        end
        #
        if  settings.calcT2
            calcT2 = true;    matrixT2 = zeros(ncsf,ncsf)
            for  r = 1:ncsf
                for  s = 1:ncsf
                    if  basis.csfs[r].parity  != basis.csfs[s].parity   continue    end
                    #
                    wa = compute("angular coefficients: 1-p, Grasp92", 0, 2, basis.csfs[r], basis.csfs[s])
                    for  coeff in wa
                        ja   = Basics.subshell_2j(basis.orbitals[coeff.a].subshell)
                        jb   = Basics.subshell_2j(basis.orbitals[coeff.b].subshell)
                        tamp  = InteractionStrength.hfs_t2(basis.orbitals[coeff.a], basis.orbitals[coeff.b], grid)
                        matrixT2[r,s] = matrixT2[r,s] + coeff.T * tamp
                    end
                end
            end
        else
            calcT2 = false;    matrixT2 = zeros(2,2)
        end
        #
        im = Hfs.InteractionMatrix(calcT1, calcT2, matrixT1, matrixT2)
        #
        return( im )
    end

    """
    `Hfs.computeOutcomes(multiplet::Multiplet, nm::Nuclear.Model, grid::Radial.Grid, settings::Hfs.Settings; output=true)`
        ... to compute (as selected) the HFS A and B parameters as well as hyperfine energy splittings for the levels
            of the given multiplet and as specified by the given settings. The results are printed in neat tables to
            screen and, if requested, an arrays{Hfs.Outcome,1} with all the results are returned.
    """
    function computeOutcomes(multiplet::Multiplet, nm::Nuclear.Model, grid::Radial.Grid, settings::Hfs.Settings; output=true)
        println("")
        printstyled("Hfs.computeOutcomes(): The computation of the Hyperfine amplitudes and parameters starts now ... \n", color=:light_green)
        printstyled("------------------------------------------------------------------------------------------------ \n", color=:light_green)
        println("")
        outcomes = Hfs.determineOutcomes(multiplet, settings)
        # Display all selected levels before the computations start
        if  settings.printBefore    Hfs.displayOutcomes(outcomes)    end
        # Calculate all amplitudes and requested properties
        im = computeInteractionMatrix(multiplet.levels[1].basis, grid, settings)
        newOutcomes = Hfs.Outcome[]
        for  outcome in outcomes
            newOutcome = Hfs.computeAmplitudesProperties(outcome, grid, settings, im)
            push!( newOutcomes, newOutcome)
        end
        # Print all results to screen
        Hfs.displayResults(stdout, newOutcomes, nm, settings)
        # Compute and display the non-diagonal hyperfine amplitudes, if requested
        if  settings.calcNondiagonal    Hfs.displayNondiagonal(stdout, multiplet, grid, settings)   end
        printSummary, iostream = Defaults.getDefaults("summary flag/stream")
        if  printSummary
            Hfs.displayResults(iostream, newOutcomes, nm, settings)
            if  settings.calcNondiagonal    Hfs.displayNondiagonal(iostream, multiplet, grid, settings)   end
        end
        #
        if    output    return( newOutcomes )
        else            return( nothing )
        end
    end

    """
    `Hfs.defineHyperfineBasis(multiplet::Multiplet, nm::Nuclear.Model, settings::Hfs.Settings)`
        ... to define/set-up an atomic hyperfine (IJF-coupled) basis for the given electronic multipet;
            a hfsBasis::IJF_Basis is returned.
    """
    function  defineHyperfineBasis(multiplet::Multiplet, nm::Nuclear.Model, settings::Hfs.Settings)
        function  display_ijfVector(i::Int64, vector::Hfs.IJF_Vector)
            si = string(i);   ni = length(si);    sa = repeat(" ", 5);    sa = sa[1:5-ni] * si * ")  "
            sa = sa * "[" * string( LevelSymmetry(vector.levelJ.J, vector.levelJ.parity) ) * "] " * string(vector.F) * repeat(" ", 4)
            return( sa )
        end

        vectors = IJF_Vector[]
        #
        for i = 1:length(multiplet.levels)
            Flist = Basics.oplus(nm.spinI, multiplet.levels[i].J)
            for  F in Flist
                push!(vectors,  IJF_Vector(nm.spinI, F, multiplet.levels[i]) )
            end
        end
        #
        hfsBasis = IJF_Basis(vectors, multiplet.levels[1].basis)
        #
        println(" ")
        println("  Construction of a atomic hyperfine (IJF-coupled) basis of dimension $(length(hfsBasis.vectors)), based on " *
                "$(length(multiplet.levels)) electronic levels and nuclear spin $(hfsBasis.vectors[1].I).")
        println("  Basis vectors are defined as [J^P] F: \n")
        sa = "  "
        for  k = 1:length(hfsBasis.vectors)
            if  length(sa) > 100    println(sa);   sa = "  "   end
            sa = sa * display_ijfVector(k, hfsBasis.vectors[k])
        end
        if   length(sa) > 5    println(sa)   end
        println(" ")

        return( hfsBasis )
    end

    """
    `Hfs.determineOutcomes(multiplet::Multiplet, settings::Hfs.Settings)`
        ... to determine a list of Outcomes's for the computation of HFS A- and B-parameters for the given multiplet.
            It takes into account the particular selections and settings. An Array{Hfs.Outcome,1} is returned. Apart from the
            level specification, all physical properties are set to zero during the initialization process.
    """
    function  determineOutcomes(multiplet::Multiplet, settings::Hfs.Settings)
        outcomes = Hfs.Outcome[]
        for  level  in  multiplet.levels
            if  Basics.selectLevel(level, settings.levelSelection)
                push!( outcomes, Hfs.Outcome(level, 0., 0., 0., 0., AngularJ64(0), false, false) )
            end
        end
        return( outcomes )
    end

    """
    `Hfs.displayNondiagonal(stream::IO, multiplet::Multiplet, grid::Radial.Grid, settings::Hfs.Settings)`
        ... to compute and display all non-diagonal hyperfine amplitudes for the selected levels. A small neat table of
            all (pairwise) hyperfine amplitudes is printed but nothing is returned otherwise.
    """
    function  displayNondiagonal(stream::IO, multiplet::Multiplet, grid::Radial.Grid, settings::Hfs.Settings)
        # Determine pairs to be calculated
        pairs = Tuple{Int64,Int64}[]
        for  (f, fLevel)  in  enumerate(multiplet.levels)
            for  (i, iLevel)  in  enumerate(multiplet.levels)
                if  Basics.selectLevel(fLevel, settings.levelSelection)  &&   Basics.selectLevel(iLevel, settings.levelSelection)
                    push!( pairs, (f,i) )
                end
            end
        end
        #
        nx = 107
        println(stream, " ")
        println(stream, "  Selected (non-) diagonal hyperfine amplitudes:")
        println(stream, " ")
        println(stream, "  ", TableStrings.hLine(nx))
        sa = "  "
        sa = sa * TableStrings.center(10, "Level_f"; na=2)
        sa = sa * TableStrings.center(10, "Level_i"; na=2)
        sa = sa * TableStrings.center(10, "J^P_f";   na=3)
        sa = sa * TableStrings.center(57, "T1   --   Amplitudes   --   T2"; na=4);
        sa = sa * TableStrings.center(10, "J^P_f";   na=4)
        println(stream, sa);    println(stream, "  ", TableStrings.hLine(nx))
        #
        for  (f,i) in pairs
            sa   = "  ";
            sa   = sa * TableStrings.center(10, string(f); na=2)
            sa   = sa * TableStrings.center(10, string(i); na=2)
            symf = LevelSymmetry( multiplet.levels[f].J, multiplet.levels[f].parity)
            symi = LevelSymmetry( multiplet.levels[i].J, multiplet.levels[i].parity)
            sa   = sa * TableStrings.center(10, string(symf); na=4)
            T1   = Hfs.amplitude("T^(1) amplitude", multiplet.levels[f], multiplet.levels[i], grid, printout=false)
            T2   = Hfs.amplitude("T^(2) amplitude", multiplet.levels[f], multiplet.levels[i], grid, printout=false)
            sa   = sa * @sprintf("%.5e %s %.5e", T1.re, "  ", T1.im) * "    "
            sa   = sa * @sprintf("%.5e %s %.5e", T2.re, "  ", T2.im) * "    "
            sa   = sa * TableStrings.center(10, string(symi); na=4)
            println(stream, sa )
        end
        println(stream, "  ", TableStrings.hLine(nx))
        #
        return( nothing )
    end

    """
    `Hfs.displayOutcomes(outcomes::Array{Hfs.Outcome,1})`
        ... to display a list of levels that have been selected for the computations A small neat table of all
            selected levels and their energies is printed but nothing is returned otherwise.
    """
    function  displayOutcomes(outcomes::Array{Hfs.Outcome,1})
        nx = 43
        println(" ")
        println("  Selected HFS levels:")
        println(" ")
        println("  ", TableStrings.hLine(nx))
        sa = "  ";   sb = "  "
        sa = sa * TableStrings.center(10, "Level"; na=2);                             sb = sb * TableStrings.hBlank(12)
        sa = sa * TableStrings.center(10, "J^P";   na=4);                             sb = sb * TableStrings.hBlank(14)
        sa = sa * TableStrings.center(14, "Energy"; na=4);
        sb = sb * TableStrings.center(14, TableStrings.inUnits("energy"); na=4)
        println(sa);    println(sb);    println("  ", TableStrings.hLine(nx))
        #
        for  outcome in outcomes
            sa  = "  ";    sym = LevelSymmetry( outcome.Jlevel.J, outcome.Jlevel.parity)
            sa = sa * TableStrings.center(10, TableStrings.level(outcome.Jlevel.index); na=2)
            sa = sa * TableStrings.center(10, string(sym); na=4)
            sa = sa * @sprintf("%.8e", Defaults.convertUnits("energy: from atomic", outcome.Jlevel.energy)) * "    "
            println( sa )
        end
        println("  ", TableStrings.hLine(nx))
        println(" ")
        #
        return( nothing )
    end

    """
    `Hfs.displayResults(stream::IO, outcomes::Array{Hfs.Outcome,1}, nm::Nuclear.Model, settings::Hfs.Settings)`
        ... to display the energies, A- and B-values, Delta E_F energy shifts, etc. for the selected levels. All nuclear
            parameters are taken from the nuclear model. A neat table is printed but nothing is returned otherwise.
    """
    function  displayResults(stream::IO, outcomes::Array{Hfs.Outcome,1}, nm::Nuclear.Model, settings::Hfs.Settings)
        nx = 117
        println(stream, " ")
        println(stream, "  HFS parameters and amplitudes:")
        println(stream, " ")
        println(stream, "  ", TableStrings.hLine(nx))
        sa = "  ";   sb = "  "
        sa = sa * TableStrings.center(10, "Level"; na=2);                             sb = sb * TableStrings.hBlank(12)
        sa = sa * TableStrings.center(10, "J^P";   na=4);                             sb = sb * TableStrings.hBlank(14)
        sa = sa * TableStrings.center(14, "Energy"; na=4);
        sb = sb * TableStrings.center(14, TableStrings.inUnits("energy"); na=4)
        sa = sa * TableStrings.center(31, "A/mu [mu_N]  --  HFS  --  B/Q [barn]"; na=4);
        sb = sb * TableStrings.center(31, TableStrings.inUnits("energy"); na=4)
        sa = sa * TableStrings.center(32, "T1 -- Amplitudes -- T2"    ; na=4);        sb = sb * TableStrings.hBlank(36)
        println(stream, sa);    println(stream, sb);    println(stream, "  ", TableStrings.hLine(nx))
        #
        for  outcome in outcomes
            sa  = "  ";    sym = LevelSymmetry( outcome.Jlevel.J, outcome.Jlevel.parity)
            sa = sa * TableStrings.center(10, TableStrings.level(outcome.Jlevel.index); na=2)
            sa = sa * TableStrings.center(10, string(sym); na=4)
            energy = outcome.Jlevel.energy
            sa = sa * @sprintf("%.8e", Defaults.convertUnits("energy: from atomic", energy))           * "      "
            wa = Defaults.convertUnits("energy: from atomic", outcome.AIoverMu) / nm.spinI.num * nm.spinI.den
            wa = wa / (2 * 1836.15267 ) * Defaults.getDefaults("alpha")  # take 2m_p and alpha into account
            sa = sa * @sprintf("%.8e", wa)                                                   * "    "
            wa = Defaults.convertUnits("energy: from atomic", outcome.BoverQ)
            wa = wa / Defaults.convertUnits("cross section: from atomic to barn", 1.0)  # take Q [barn] into account
            sa = sa * @sprintf("%.8e", wa)                                                   * "    "
            sa = sa * @sprintf("%.8e %s %.8e", outcome.amplitudeT1.re, "  ", outcome.amplitudeT2.re) * "    "
            println(stream, sa )
        end
        println(stream, "  ", TableStrings.hLine(nx))
        #
        #
        if !settings.printDeltaEF   return( nothing )   end
        #
        # Printout the Delta E_F energy shifts of the hyperfine levels |alpha F> with regard to the (electronic) levels |alpha J>
        nx = 90
        println(stream, " ")
        println(stream, "  HFS Delta E_F energy shifts with regard to the (electronic) level energies E_J:")
        println(stream, " ")
        println(stream, "    Nuclear spin I:                        $(nm.spinI) ")
        println(stream, "    Nuclear magnetic-dipole moment    mu = $(nm.mu)    ")
        println(stream, "    Nuclear electric-quadrupole moment Q = $(nm.Q)     ")
        println(stream, " ")
        println(stream, "  ", TableStrings.hLine(nx))
        sa = "  ";   sb = "  "
        sa = sa * TableStrings.center(10, "Level"; na=2);                             sb = sb * TableStrings.hBlank(12)
        sa = sa * TableStrings.center(10, "J^P";   na=4);                             sb = sb * TableStrings.hBlank(14)
        sa = sa * TableStrings.center(14, "Energy"; na=4);                            sb = sb * TableStrings.hBlank(18)
        sa = sa * TableStrings.center(10, "F^P";   na=4);                             sb = sb * TableStrings.hBlank(14)
        sa = sa * TableStrings.center(14, "Delta E_F"; na=4);
        sb = sb * TableStrings.center(14, TableStrings.inUnits("energy"); na=4)
        sa = sa * TableStrings.center(14, "C factor"; na=4);
        println(stream, sa);    println(stream, sb);    println(stream, "  ", TableStrings.hLine(nx))
        #
        for  outcome in outcomes
            sa  = "  ";    sym = LevelSymmetry( outcome.Jlevel.J, outcome.Jlevel.parity)
            sa = sa * TableStrings.center(10, TableStrings.level(outcome.Jlevel.index); na=2)
            sa = sa * TableStrings.center(10, string(sym); na=4)
            energy = outcome.Jlevel.energy
            #
            sa = sa * @sprintf("%.8e", Defaults.convertUnits("energy: from atomic", energy))           * "    "
            J     = AngularMomentum.oneJ(outcome.Jlevel.J);   spinI = AngularMomentum.oneJ(nm.spinI)
            Flist = Basics.oplus(nm.spinI, outcome.Jlevel.J)
            first = true
            for  Fang in Flist
                Fsym    = LevelSymmetry(Fang, outcome.Jlevel.parity)
                F       = AngularMomentum.oneJ(Fang)
                Cfactor = F*(F+1) - J*(J+1) - spinI*(spinI+1)
                energy  = outcome.AIoverMu * nm.mu / spinI * Cfactor / 2.
                if  abs(outcome.BoverQ) > 1.0e-10
                    energy  = energy +  outcome.BoverQ * nm.Q * 3/4 * (Cfactor*(Cfactor+1) - spinI*(spinI+1)*J*(J+1) ) /
                                        ( 2spinI*(2spinI-1)*J*(2J-1) )
                end
                sb = TableStrings.center(10, string(Fsym); na=2)
                sb = sb * TableStrings.flushright(16, @sprintf("%.8e", Defaults.convertUnits("energy: from atomic", energy))) * "    "
                sb = sb * TableStrings.flushright(12, @sprintf("%.5e", Cfactor))
                #
                if   first    println(stream,  sa*sb );   first = false
                else          println(stream,  TableStrings.hBlank( length(sa) ) * sb )
                end
            end
        end
        println(stream, "  ", TableStrings.hLine(nx))
        #
        return( nothing )
    end

end # module


HEIGHT=400
WIDHT=400
BACKGROUND= colorant"purple"
alien_actor=Actor("alien.png")
alien_actor.pos= (120,180)
text_actor=TextActor("hello boys","moonhouse")

text_actor.pos=(120,120)
rect=Rect(50,100,20,50)
cir = Circle(330,80,20)

i=0
function draw()
    draw(alien_actor)
    draw(text_actor)
    draw( Line(50+i,100,350+i,100),colorant"yellow")
    draw(rect,colorant"white",fill=true)
    draw(cir,colorant"black",fill=true)
end

function update()
    alien_actor.x=alien_actor.x+2
    alien_actor.y+=1
    text_actor.y+=1
    global i
    i+=10
end


# Avoid ambiguity
function update_batch_vertex(g::GATConv, M::AbstractMatrix, X::CuMatrix, u)
    M = convert(typeof(X), M)
    update_batch_vertex(g, M, X, u)
end

function update_batch_vertex(g::GATConv, M::CuMatrix, X::CuMatrix, u)
    M = M .+ g.bias
    if !g.concat
        N = size(M, 2)
        M = reshape(mean(reshape(M, :, g.heads, N), dims=2), :, N)
    end
    return M
end


# submarine cablesb

Pkg.add("Downloads")

using GLMakie, CairoMakie, Downloads
using GeoMakie, GeoMakie.GeoJSON, GeoMakie.GeoInterface
CairoMakie.activate!()
let
    # data from
    # https://github.com/telegeography/www.submarinecablemap.com
    urlPoints = "https://raw.githubusercontent.com/telegeography/www.submarinecablemap.com/master/web/public/api/v3/landing-point/landing-point-geo.json"
    urlCables = "https://raw.githubusercontent.com/telegeography/www.submarinecablemap.com/master/web/public/api/v3/cable/cable-geo.json"

    landPoints = Downloads.download(urlPoints, IOBuffer())
    landCables = Downloads.download(urlCables, IOBuffer())

    land_geoPoints = GeoJSON.read(seekstart(landPoints))
    land_geoCables = GeoJSON.read(seekstart(landCables))

    toPoints = GeoMakie.geo2basic(land_geoPoints)
    #toLines = GeoMakie.geo2basic(land_geoCables) # this should probably be supported.
    feat = GeoInterface.features(land_geoCables)
    toLines = GeoInterface.coordinates.(GeoInterface.geometry.(feat))
    splitLines = []
    for i in 1:length(toLines)
        for j in 1:length(toLines[i])
            push!(splitLines, Point2f.(toLines[i][j]))
        end
    end

    function plotCables()
        fig, ax, = CairoMakie.scatter(toPoints; markersize= 5, color = 1:length(toPoints),
            colormap = :plasma, figure = (;resolution = (1200,800), fontsize = 24))
        [CairoMakie.lines!(ax, splitLines[i]; linewidth=0.85) for i in 1:length(splitLines)]
        CairoMakie.lines!(GeoMakie.coastlines(); color = :white, linewidth = 0.35)
        limits!(ax,-185,185,-95,95)
        ax.xticks = -180:60:180
        ax.yticks = -90:30:90
        ax.xtickformat = "{:d}ᵒ"
        ax.ytickformat = "{:d}ᵒ"
        fig
    end
    fig = with_theme(plotCables, theme_dark())
    display(fig)
end


module RcSetup
using Parameters
import DataStructures: OrderedDict
export Setup, Port, all_ports, OrderedDict

module Port
export PressPorts, all_ports
@enum PressPorts in1 in2 in3 in4 out1 out2 out3 out4 ice1 ice2 ice3 ice4
const all_ports = [in1, in2, in3, in4, ice1, ice2, ice3, ice4, out1, out2, out3, out4]
end
"Distance between fixed installed ports"
const def_portdists = [0.88, 0.44, 0.44, 1.68, 0.34, 0.34, 0.68]

# TODO: figure something out for pressure ports in ice:  The change depending on time

const P2C = OrderedDict{Port.PressPorts,Vector{Int}}
@with_kw struct Setup
    drawing_pdf::String # path to pdf
    port2channels::P2C # mapping physical port to channels
                                                            # keep the ports in order
    @assert length(used_channels(port2channels))==length(unique(used_channels(port2channels)))
    portdists::Vector{Float64}=def_portdists # needs to be in order of keys(port2channels)
end
"Return the used measurement channels"
used_channels(di::P2C) = sort(vcat(values(di)...))
used_channels(setup::Setup) = used_channels(setup.port2channels)
"Return the used measurement ports"
used_ports(di::P2C) = collect(keys(di))
used_ports(setup::Setup) = used_ports(setup.port2channels)

end


using Test
using CompilerPluginTools

const GLOBAL_CONST = 2.0

struct Foo
    x::Int
end

@testset "inline_const" begin
    ir = @make_ircode begin
        Expr(:call, Core.Intrinsics.abs_float, 1.0)::Float64
        GlobalRef(Main, :GLOBAL_CONST)::Float64
        ReturnNode(SSAValue(1))::Float64
    end

    ir = inline_const!(ir)
    @test ir.stmts[1][:inst] == 1.0
    @test ir.stmts[1][:type] == Const(1.0)
    @test ir.stmts[2][:inst] == 2.0
    @test ir.stmts[2][:type] == Const(2.0)

    ir = @make_ircode begin
        Expr(:call, Core.Intrinsics.abs_float, 1.0)::Float64
        Expr(:new, Foo, 2)::Foo
        ReturnNode(SSAValue(1))::Float64
    end

    ir = inline_const!(ir)

    @test ir.stmts[2][:inst] == QuoteNode(Foo(2))
    @test ir.stmts[2][:type] == Const(Foo(2))

    ir = @make_ircode begin
        QuoteNode(1)::Int
        Expr(:new, Foo, SSAValue(1))::Foo
        ReturnNode(SSAValue(1))::Int
    end
    ir = inline_const!(ir)
    @test ir.stmts[2][:inst] == QuoteNode(Foo(1))
    @test ir.stmts[2][:type] == Const(Foo(1))

    ir = @make_ircode begin
        Expr(:call, Core.tuple, 1, 2, 3)::Tuple{Int, Int, Int}
        ReturnNode(SSAValue(1))::Tuple{Int, Int, Int}
    end
    ir = inline_const!(ir)
    @test ir.stmts[1][:inst] == (1, 2, 3)
    @test ir.stmts[1][:type] == Const((1, 2, 3))
end

ir = @make_ircode begin
    Expr(:call, Core.Intrinsics.abs_float, 1.0)::Float64
    Expr(:new, Foo, 2)::Foo
    ReturnNode(SSAValue(1))::Float64
end

ir = inline_const!(ir)

@testset "permute_stmts!" begin
    ir = @make_ircode begin
        QuoteNode(1.0)::Const(1.0)
        Expr(:call, sin, SSAValue(1))::Float64
        QuoteNode(2.0)::Const(2.0)
        Expr(:call, sin, SSAValue(3))::Float64
        Expr(:call, <, SSAValue(4), QuoteNode(1.0))::Bool
        GotoIfNot(SSAValue(5), 7)
        Expr(:call, +, SSAValue(2), SSAValue(4))::Float64
        ReturnNode(SSAValue(6))::Float64
    end

    ir = permute_stmts!(ir, [1, 3, 2, 4, 5, 6, 7, 8])
    @test ir.stmts[1][:inst] == QuoteNode(1.0)
    @test ir.stmts[2][:inst] == QuoteNode(2.0)
    @test ir.stmts[3][:inst] == Expr(:call, sin, SSAValue(1))
    @test ir.stmts[4][:inst] == Expr(:call, sin, SSAValue(2))
    @test ir.stmts[5][:inst] == Expr(:call, <, SSAValue(4), QuoteNode(1.0))
    @test ir.stmts[6][:inst] == GotoIfNot(SSAValue(5), 2)
    @test ir.stmts[7][:inst] == Expr(:call, +, SSAValue(3), SSAValue(4))
    @test ir.stmts[8][:inst] == ReturnNode(SSAValue(6))
end

function foo(x)
    2x
end

@testset "const_invoke!" begin
    mi = method_instances(foo, (Float64, ))[1]

    ir = @make_ircode begin
        Expr(:invoke, mi, GlobalRef(Main, :foo), 2.2)::Float64
        Expr(:invoke, mi, GlobalRef(Main, :foo), Argument(2))::Float64
        ReturnNode(SSAValue(1))::Float64
    end

    ir = const_invoke!(ir, GlobalRef(Main, :foo)) do x
        3x
    end

    @test ir.stmts[1][:inst] ≈ 3 * 2.2
    @test ir.stmts[2][:inst] == Expr(:invoke, mi, GlobalRef(Main, :foo), Argument(2))
end


include("TwoPartCost.jl")
include("ThreePartCost.jl")
include("TechHydro.jl")
include("TechRenewable.jl")
include("TechThermal.jl")
include("Bus.jl")
include("Arc.jl")
include("Line.jl")
include("MonitoredLine.jl")
include("PhaseShiftingTransformer.jl")
include("TapTransformer.jl")
include("Transformer2W.jl")
include("HVDCLine.jl")
include("VSCDCLine.jl")
include("InterruptibleLoad.jl")
include("FixedAdmittance.jl")
include("PowerLoad.jl")
include("HydroDispatch.jl")
include("HydroFix.jl")
include("HydroStorage.jl")
include("RenewableDispatch.jl")
include("RenewableFix.jl")
include("ThermalStandard.jl")
include("LoadZones.jl")
include("GenericBattery.jl")
include("ProportionalReserve.jl")
include("StaticReserve.jl")
include("Transfer.jl")

export get_Y
export get_activepower
export get_activepower_flow
export get_activepowerlimits
export get_activepowerlimits_from
export get_activepowerlimits_to
export get_angle
export get_anglelimits
export get_arc
export get_available
export get_b
export get_basevoltage
export get_bus
export get_buses
export get_bustype
export get_capacity
export get_contributingdevices
export get_efficiency
export get_energy
export get_fixed
export get_flowlimits
export get_from
export get_fuel
export get_initial_storage
export get_inputactivepowerlimits
export get_internal
export get_inverter_firing_angle
export get_inverter_taplimits
export get_inverter_xrc
export get_loss
export get_maxactivepower
export get_maxreactivepower
export get_model
export get_name
export get_number
export get_op_cost
export get_outputactivepowerlimits
export get_powerfactor
export get_primaryshunt
export get_primemover
export get_r
export get_ramplimits
export get_rate
export get_rating
export get_reactivepower
export get_reactivepower_flow
export get_reactivepowerlimits
export get_reactivepowerlimits_from
export get_reactivepowerlimits_to
export get_rectifier_firing_angle
export get_rectifier_taplimits
export get_rectifier_xrc
export get_requirement
export get_shutdn
export get_startup
export get_storagecapacity
export get_tap
export get_tech
export get_timeframe
export get_timelimits
export get_to
export get_variable
export get_voltage
export get_voltagelimits
export get_x
export get_α


# """
#     MOI.add_constrained_variable(model::Optimizer, set::MOI.Interval{Int})

# Add a variable to the model, enforcing to give an interval with it as you cannot create variables without bounds in SeaPearl.
# """

function MOI.add_variable(model::Optimizer)
    id = length(keys(model.moimodel.variables)) + 1

    vi = MOI.VariableIndex(id)

    moiv = MOIVariable("", nothing, nothing, vi)
    push!(model.moimodel.variables, moiv)

    return vi
end


using ..ChemistryFeaturization.Utils.ElementFeatureUtils
using DataFrames
using ..ChemistryFeaturization: elements
using ..ChemistryFeaturization.AbstractType: AbstractCodec, AbstractAtoms
using ..ChemistryFeaturization.Codec: OneHotOneCold

include("abstractfeatures.jl")

# TODO - consider edge cases in constructor. add this stuff into modulify.

# TODO: figure out what scheme would look like that is flexible to direct-value encoding (may just need a different feature type since it'll have to handle normalization, etc. too)
"""
    ElementFeatureDescriptor

Describe features associated with individual atoms that depend only upon their elemental identity

## Fields
- `name::String`: Name of the feature
- `encoder_decoder::AbstractCodec`: Codec defined which handles the feature's encoding and decoding logic
- `categorical::Bool`: flag for whether the feature is categorical or continuous-valued
- `lookup_table::DataFrame`: table containing values of feature for every encodable element
"""
struct ElementFeatureDescriptor <: AbstractAtomFeatureDescriptor
    name::String
    encoder_decoder::AbstractCodec
    categorical::Bool
    lookup_table::DataFrame
end

function ElementFeatureDescriptor(feature_name::String, encoder_decoder::AbstractCodec)
    lookup_table = atom_data_df

    colnames = names(lookup_table)
    @assert feature_name in colnames && "Symbol" in colnames "Your lookup table must have a column called :Symbol and one with the same name as your feature to be usable!"

    lookup_table = lookup_table[:, ["Symbol", feature_name]]
    dropmissing!(lookup_table)

    ElementFeatureDescriptor(
        feature_name,
        encoder_decoder,
        default_categorical(feature_name, lookup_table),
        lookup_table,
    )
end

"""
    ElementFeatureDescriptor(feature_name, lookup_table, categorical, contextual, length, encodable_elements)

Construct a feature object that encodes features associated with individual atoms that depend only upon their elemental identity.
If a Codec isn't explicity specified, [OneHotOneCold](@ref) with [default_efd_encode](@ref) and [default_efd_decode](@ref)
as the encoding and decoding functions respectively is the default choice.

## Arguments
- `name::String`: the name of the feature
- `lookup_table::DataFrame`: table containing values of feature for every encodable element
- `nbins::Integer`: Number of bins to use for one-cold decoding of continuous-valued features
- `logspaced::Bool`: whether onehot-style bins should be logarithmically spaced or not
- `categorical::Bool`: flag for whether the feature is categorical or continuous-valued
"""
function ElementFeatureDescriptor(
    feature_name::String,
    lookup_table::DataFrame = atom_data_df;
    nbins::Integer = default_nbins,
    logspaced::Bool = default_log(feature_name, lookup_table),
    categorical::Bool = default_categorical(feature_name, lookup_table),
)
    colnames = names(lookup_table)
    @assert feature_name in colnames && "Symbol" in colnames "Your lookup table must have a column called :Symbol and one with the same name as your feature to be usable!"

    lookup_table = lookup_table[:, ["Symbol", feature_name]]
    dropmissing!(lookup_table)

    ElementFeatureDescriptor(
        feature_name,
        OneHotOneCold(default_efd_encode, default_efd_decode, nbins, logspaced),
        categorical,
        lookup_table,
    )
end

# pretty printing, short version
Base.show(io::IO, efd::ElementFeatureDescriptor) = print(io, "ElementFeature $(efd.name)")

# pretty printing, long version
function Base.show(io::IO, ::MIME"text/plain", efd::ElementFeatureDescriptor)
    st = "ElementFeature $(efd.name):\n   categorical: $(efd.categorical)\n   encoded length: $(output_shape(efd))"
    print(io, st)
end

encodable_elements(efd::ElementFeatureDescriptor) = efd.lookup_table[:, :Symbol]

function encodable_elements(feature_name::String, lookup_table::DataFrame = atom_data_df)
    info = lookup_table[:, [Symbol(feature_name), :Symbol]]
    return info[
        findall(x -> !ismissing(x), getproperty(info, Symbol(feature_name))),
        :Symbol,
    ]
end

function (efd::ElementFeatureDescriptor)(a::AbstractAtoms)
    @assert all([el in encodable_elements(efd) for el in elements(a)]) "Feature $(efd.name) cannot encode some element(s) in this structure!"
    efd.encoder_decoder(efd, a)
end

(ed::OneHotOneCold)(efd::ElementFeatureDescriptor, a::AbstractAtoms) =
    ed.encode_f(efd, a, ed.nbins, ed.logspaced)

(ed::OneHotOneCold)(efd::ElementFeatureDescriptor, encoded_feature) =
    ed.decode_f(efd, encoded_feature, ed.nbins, ed.logspaced)

"""
    output_shape(efd::ElementFeatureDescriptor)

Get the output-shape for an ElementFeatureDescriptor object using the logic assoicated with its
Codec.
"""
function output_shape(efd::ElementFeatureDescriptor, ed::OneHotOneCold)
    return efd.categorical ? length(unique(efd.lookup_table[:, Symbol(efd.name)])) :
           ed.nbins
end

"""
    default_efd_encode(efd::ElementFeatureDescriptor, a::AbstractAtoms, nbins::Integer, logspaced::Bool)

Default one-hot encoding function for an ElementFeatureDescriptor object.
"""
function default_efd_encode(
    efd::ElementFeatureDescriptor,
    a::AbstractAtoms,
    nbins::Integer,
    logspaced::Bool,
)
    reduce(
        hcat,
        map(
            e -> onehot_lookup_encoder(
                e,
                efd.name,
                efd.lookup_table;
                nbins = nbins,
                logspaced = logspaced,
                categorical = efd.categorical,
            ),
            elements(a),
        ),
    )
end

"""
    default_efd_decode(efd::ElementFeatureDescriptor, encoded_feature, nbins::Integer, logspaced::Bool)

Default one-cold decoding logic for an ElementFeatureDescriptor object.
"""
default_efd_decode(efd::ElementFeatureDescriptor, encoded_feature, nbins, logspaced) =
    onecold_decoder(
        encoded_feature,
        efd.name,
        efd.lookup_table;
        nbins = nbins,
        logspaced = logspaced,
        categorical = efd.categorical,
    )


# Julia wrapper for header: /usr/include/scip/pub_nlp.h
# Automatically generated using Clang.jl wrap_c

function SCIPexprtreeGetVars(tree)
    ccall((:SCIPexprtreeGetVars, libscip), Ptr{Ptr{SCIP_VAR}}, (Ptr{SCIP_EXPRTREE},), tree)
end

function SCIPexprtreeSetVars(tree, nvars, vars)
    ccall((:SCIPexprtreeSetVars, libscip), SCIP_RETCODE, (Ptr{SCIP_EXPRTREE}, Cint, Ptr{Ptr{SCIP_VAR}}), tree, nvars, vars)
end

function SCIPexprtreeAddVars(tree, nvars, vars)
    ccall((:SCIPexprtreeAddVars, libscip), SCIP_RETCODE, (Ptr{SCIP_EXPRTREE}, Cint, Ptr{Ptr{SCIP_VAR}}), tree, nvars, vars)
end

function SCIPexprtreePrintWithNames(tree, messagehdlr, file)
    ccall((:SCIPexprtreePrintWithNames, libscip), SCIP_RETCODE, (Ptr{SCIP_EXPRTREE}, Ptr{SCIP_MESSAGEHDLR}, Ptr{FILE}), tree, messagehdlr, file)
end

function SCIPexprtreeFindVar(tree, var)
    ccall((:SCIPexprtreeFindVar, libscip), Cint, (Ptr{SCIP_EXPRTREE}, Ptr{SCIP_VAR}), tree, var)
end

function SCIPnlrowGetConstant(nlrow)
    ccall((:SCIPnlrowGetConstant, libscip), Cdouble, (Ptr{SCIP_NLROW},), nlrow)
end

function SCIPnlrowGetNLinearVars(nlrow)
    ccall((:SCIPnlrowGetNLinearVars, libscip), Cint, (Ptr{SCIP_NLROW},), nlrow)
end

function SCIPnlrowGetLinearVars(nlrow)
    ccall((:SCIPnlrowGetLinearVars, libscip), Ptr{Ptr{SCIP_VAR}}, (Ptr{SCIP_NLROW},), nlrow)
end

function SCIPnlrowGetLinearCoefs(nlrow)
    ccall((:SCIPnlrowGetLinearCoefs, libscip), Ptr{Cdouble}, (Ptr{SCIP_NLROW},), nlrow)
end

function SCIPnlrowGetNQuadVars(nlrow)
    ccall((:SCIPnlrowGetNQuadVars, libscip), Cint, (Ptr{SCIP_NLROW},), nlrow)
end

function SCIPnlrowGetQuadVars(nlrow)
    ccall((:SCIPnlrowGetQuadVars, libscip), Ptr{Ptr{SCIP_VAR}}, (Ptr{SCIP_NLROW},), nlrow)
end

function SCIPnlrowSearchQuadVar(nlrow, var)
    ccall((:SCIPnlrowSearchQuadVar, libscip), Cint, (Ptr{SCIP_NLROW}, Ptr{SCIP_VAR}), nlrow, var)
end

function SCIPnlrowGetNQuadElems(nlrow)
    ccall((:SCIPnlrowGetNQuadElems, libscip), Cint, (Ptr{SCIP_NLROW},), nlrow)
end

function SCIPnlrowGetQuadElems(nlrow)
    ccall((:SCIPnlrowGetQuadElems, libscip), Ptr{SCIP_QUADELEM}, (Ptr{SCIP_NLROW},), nlrow)
end

function SCIPnlrowGetQuadData(nlrow, nquadvars, quadvars, nquadelems, quadelems)
    ccall((:SCIPnlrowGetQuadData, libscip), Cvoid, (Ptr{SCIP_NLROW}, Ptr{Cint}, Ptr{Ptr{Ptr{SCIP_VAR}}}, Ptr{Cint}, Ptr{Ptr{SCIP_QUADELEM}}), nlrow, nquadvars, quadvars, nquadelems, quadelems)
end

function SCIPnlrowGetExprtree(nlrow)
    ccall((:SCIPnlrowGetExprtree, libscip), Ptr{SCIP_EXPRTREE}, (Ptr{SCIP_NLROW},), nlrow)
end

function SCIPnlrowGetLhs(nlrow)
    ccall((:SCIPnlrowGetLhs, libscip), Cdouble, (Ptr{SCIP_NLROW},), nlrow)
end

function SCIPnlrowGetRhs(nlrow)
    ccall((:SCIPnlrowGetRhs, libscip), Cdouble, (Ptr{SCIP_NLROW},), nlrow)
end

function SCIPnlrowGetCurvature(nlrow)
    ccall((:SCIPnlrowGetCurvature, libscip), SCIP_EXPRCURV, (Ptr{SCIP_NLROW},), nlrow)
end

function SCIPnlrowSetCurvature(nlrow, curvature)
    ccall((:SCIPnlrowSetCurvature, libscip), Cvoid, (Ptr{SCIP_NLROW}, SCIP_EXPRCURV), nlrow, curvature)
end

function SCIPnlrowGetName(nlrow)
    ccall((:SCIPnlrowGetName, libscip), Cstring, (Ptr{SCIP_NLROW},), nlrow)
end

function SCIPnlrowGetNLPPos(nlrow)
    ccall((:SCIPnlrowGetNLPPos, libscip), Cint, (Ptr{SCIP_NLROW},), nlrow)
end

function SCIPnlrowIsInNLP(nlrow)
    ccall((:SCIPnlrowIsInNLP, libscip), UInt32, (Ptr{SCIP_NLROW},), nlrow)
end

function SCIPnlrowGetDualsol(nlrow)
    ccall((:SCIPnlrowGetDualsol, libscip), Cdouble, (Ptr{SCIP_NLROW},), nlrow)
end


@testset "Retrieve" begin
    datadir = joinpath(@__DIR__,"data")

    @testset "ERA5 monthly preasure data" begin
        filepath = joinpath(datadir, "era5.grib")
        response = CDSAPI.retrieve("reanalysis-era5-pressure-levels-monthly-means",
            py2ju("""{
                'format': 'grib',
                'product_type': 'monthly_averaged_reanalysis',
                'variable': 'divergence',
                'pressure_level': '1',
                'year': '2020',
                'month': '06',
                'area': [
                    90, -180, -90,
                    180,
                ],
                'time': '00:00',
            }"""),
            filepath)

        @test typeof(response) <: Dict
        @test response["content_type"] == "application/x-grib"
        @test isfile(filepath)

        GribFile(filepath) do datafile
            data = Message(datafile)
            @test data["name"] == "Divergence"
            @test data["level"] == 1
            @test data["year"] == 2020
            @test data["month"] == 6
        end
        rm(filepath)
    end

    @testset "Europe water quantity data" begin
        filepath = joinpath(datadir, "ewq.zip")
        response = CDSAPI.retrieve("sis-water-quantity-swicca",
            py2ju("""{
                'variable': 'river_flow',
                'time_aggregation': 'annual_maximum',
                'horizontal_aggregation': 'catchments',
                'emissions_scenario': 'rcp_2_6',
                'period': '2071_2100',
                'return_period': '100',
                'format': 'zip',
            }"""),
            filepath)

        @test typeof(response) <: Dict
        @test response["content_type"] == "application/zip"
        @test isfile(filepath)

        # extract contents
        zip_reader = ZipFile.Reader(filepath)
        ewq_fileio = zip_reader.files[1]
        ewq_file = joinpath(datadir, ewq_fileio.name)
        write(ewq_file, read(ewq_fileio))
        close(zip_reader)

        # test file contents
        @test ncgetatt(ewq_file, "Global", "time_coverage_start") == "20710101"
        @test ncgetatt(ewq_file, "Global", "time_coverage_end") == "21001231"
        @test ncgetatt(ewq_file, "Global", "invar_experiment_name") == "rcp26"

        # cleanup
        rm(filepath)
        rm(ewq_file)
    end

    @testset "European energy sector cimate" begin
        filepath = joinpath(datadir, "ees.tar.gz")
        response = CDSAPI.retrieve("sis-european-energy-sector",
            py2ju("""{
                'variable': 'precipitation',
                'time_aggregation': '1_year_average',
                'vertical_level': '0_m',
                'bias_correction': 'bias_adjustment_based_on_gamma_distribution',
                'format': 'tgz',
            }"""),
            filepath)

        @test typeof(response) <: Dict
        @test response["content_type"] == "application/gzip"
        @test isfile(filepath)

        # extract contents
        gzip_io = GZip.open(filepath)
        eesfile_dir = Tar.extract(gzip_io, joinpath(datadir, "ees"))
        ees_file = joinpath(eesfile_dir, readdir(eesfile_dir)[1])
        close(gzip_io)

        # test file contents
        @test ncgetatt(ees_file, "Global", "frequency") == "year"
        @test ncgetatt(ees_file, "tp", "long_name") == "precip total"

        # cleanup
        rm(filepath)
        rm(ees_file)
        rm(eesfile_dir)
    end
end


# MIT License
#
# Copyright (c) 2018 Martin Biel
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

# Integer API #
# ------------------------------------------------------------
initialize_integer_algorithm!(lshaped::AbstractLShaped) = initialize_integer_algorithm!(lshaped.integer, lshaped.structure.first_stage)
initialize_integer_algorithm!(subproblem::SubProblem) = initialize_integer_algorithm!(subproblem.integer_algorithm, subproblem)
# ------------------------------------------------------------
# Attributes #
# ------------------------------------------------------------
"""
    RawIntegerAlgorithmParameter

An optimizer attribute used for raw parameters of the integer algorithm. Defers to `RawParameter`.
"""
struct RawIntegerParameter <: IntegerParameter
    name::Any
end

include("common.jl")
include("ignore_integers.jl")
include("combinatorial_cuts.jl")
include("convexification.jl")


# This script holds measurement error, which figures
# out what EE and MM matrices to return based on the subspec of the model
function measurement_error(m::DSGEVAR{T}) where {T<:Real}
    ss = subspec(m)
    if ss == "no subspecs with measurement error yet"
    else
        EE = zeros(T, n_observables(m), n_observables(m))
        MM = zeros(T, n_observables(m), n_shocks(m))
    end

    return EE, MM
end


struct MockSolver end

mutable struct CurentState
    route_idx::Int
    v_set_idx::Int
    v_idx::Int # inside a set idx
    act_idx::Int # inside a route
    t::Float64 # inside a route
    c::Dict{String,Float64} # inside a route
    CurentState() = new(1, 1, 1, 1, 0.0, Dict{String,Float64}())
end

function create_action(cur_state::CurentState, data::RvrpInstance,
                       loc::Location, act_type::Int; req_id = "")
    return Action(string("act_", cur_state.act_idx),
                       loc.id, act_type, req_id, cur_state.t)
end

function init_route(cur_state::CurentState, data::RvrpInstance,
                    computed_data::RvrpComputedData)

    v_cat = get_vehicle_category(data.vehicle_sets[cur_state.v_set_idx],
                                      data, computed_data)
    cur_state.c = Dict{String,Float64}(k => 0.0 for k in keys(v_cat.capacities.of_vehicle))
    cur_state.t = data.vehicle_sets[cur_state.v_set_idx].work_periods[1].soft_range.lb
    loc = get_departure_locations(data.vehicle_sets[cur_state.v_set_idx], data, computed_data)[1]

    act = create_action(cur_state, data, loc, 0)
    return Route(string("r_", cur_state.route_idx), data.vehicle_sets[cur_state.v_set_idx].id, [act], 0)
end

function finalize_route(r::Route, data::RvrpInstance,
                        computed_data::RvrpComputedData,
                        cur_state::CurentState, travel_times::Array{Float64,2})
    depot_loc = get_arrival_locations(data.vehicle_sets[cur_state.v_set_idx],
                                           data, computed_data)[1]
    if !isempty(travel_times)
        prev_location_idx = computed_data.location_id_2_index[r.sequence[end].location_id]
        cur_state.t += travel_times[prev_location_idx,depot_loc.index]
    end
    cur_state.act_idx += 1
    depot_act = create_action(cur_state, data, depot_loc, 0)
    push!(r.sequence, depot_act)

    cur_state.v_idx += 1
    cur_state.route_idx += 1
    cur_state.act_idx = 1
    # Check if need to change v_set_idx every time before initialize the route
    if cur_state.v_idx > data.vehicle_sets[cur_state.v_set_idx].nb_of_vehicles_range.soft_range.ub
        cur_state.v_set_idx += 1
        if cur_state.v_set_idx > length(data.vehicle_sets)
            return false
        end
        cur_state.v_idx = 1
    end
    return true
end

function check_if_feasible(req::Request, cur_state::CurentState,
                           r::Route, data::RvrpInstance,
                           computed_data::RvrpComputedData,
                           travel_times::Array{Float64,2})

    cap_cons = get_capacity_consumptions(req, data.product_specification_classes, computed_data)

    v_cat = get_vehicle_category(data.vehicle_sets[cur_state.v_set_idx],
                                      data, computed_data)
    for (k,v) in v_cat.capacities.of_vehicle
        if cur_state.c[k] + cap_cons[k] > v_cat.capacities.of_vehicle[k]
            return false, -1, cap_cons
        end
    end

    prev_loc_idx = computed_data.location_id_2_index[r.sequence[end].location_id]
    t = cur_state.t
    if req.request_type == 0 || req.request_type == 1
        p_loc_idx = get_pickup_locations(req, data, computed_data)[1].index
        # Time of arrival
        if !isempty(travel_times)
            t += travel_times[prev_loc_idx,p_loc_idx]
        end
        t = max(t, req.pickup_time_windows[1].soft_range.lb)
        if (t > req.pickup_time_windows[1].soft_range.ub ||
            t > data.locations[p_loc_idx].opening_time_windows[1].ub)
            return false, -1, cap_cons
        end
        t += req.pickup_service_time # Leaving time
        prev_loc_idx = p_loc_idx
    end

    if req.request_type == 0 || req.request_type == 2
        d_loc_idx = get_delivery_locations(req, data, computed_data)[1].index
        if !isempty(travel_times)
            t += travel_times[prev_loc_idx,d_loc_idx]
        end
        t = max(t, req.delivery_time_windows[1].soft_range.lb)
        if (t > req.delivery_time_windows[1].soft_range.ub ||
            t > data.locations[d_loc_idx].opening_time_windows[1].ub)
            return false, -1, cap_cons
        end
        t += req.delivery_service_time
        prev_loc_idx = d_loc_idx
    end

    depot_loc_idx = get_arrival_locations(data.vehicle_sets[cur_state.v_set_idx],
                                               data, computed_data)[1].index
    return_to_base_time = 0
    if !isempty(travel_times)
        return_to_base_time = travel_times[prev_loc_idx,depot_loc_idx]
    end
    if t + return_to_base_time > data.vehicle_sets[cur_state.v_set_idx].work_periods[1].soft_range.ub
        return false, -1, cap_cons
    end

    return true, t, cap_cons

end

function build_routes(data::RvrpInstance, solver::MockSolver,
                      computed_data::RvrpComputedData)
    unassigned = String[]
    travel_times = data.travel_specifications[1].travel_time_matrix
    routes = Route[]
    cur_state = CurentState()
    route = init_route(cur_state, data, computed_data)
    req_idx = 1
    while req_idx <= length(data.requests)
        req = data.requests[req_idx]
        feas, t, cap_cons = check_if_feasible(req, cur_state, route, data, computed_data, travel_times)
        if feas
            cur_state.t = t
            if req.request_type != 0
                for k in keys(cap_cons)
                    cur_state.c[k] += cap_cons[k]
                end
            end
            if req.request_type == 0 || req.request_type == 1
                p_loc = get_pickup_locations(req, data, computed_data)[1]
                cur_state.act_idx += 1
                p_act = create_action(cur_state, data, p_loc, 1, req_id = req.id)
                push!(route.sequence, p_act)
            end

            if req.request_type == 0 || req.request_type == 2
                d_loc = get_delivery_locations(req, data, computed_data)[1]
                cur_state.act_idx += 1
                d_act = create_action(cur_state, data, d_loc, 2, req_id = req.id)
                push!(route.sequence, d_act)
            end
            req_idx += 1
        end
        # If not feasible or the last req was just inserted
        if !feas || req_idx == length(data.requests) + 1
            feas = finalize_route(route, data, computed_data, cur_state, travel_times)
            push!(routes, route)
            if !feas
                unassigned = [req.id for req in data.requests[req_idx:end]]
                break
            end
            if req_idx <= length(data.requests) # If not all requests were done
                route = init_route(cur_state, data, computed_data)
            else
                break
            end
        end
    end
    return routes, unassigned
end

function solve(data::RvrpInstance, solver::MockSolver)

    id = string(data.id, "_mock_SOL_", rand(1:10000))
    if !check_initial_feasibility(data)
        return build_empty_sol(data, id)
    end
    computed_data = build_computed_data(data)
    routes, unassigned =  build_routes(data, solver, computed_data)
    problem_id = data.id
    sol = RvrpSolution(id, problem_id, 0.0, routes, unassigned)
    compute_cost_and_scheduled_times(sol, data, computed_data)
    return sol

end

mutable struct SolutionTimes
    current::Float64
    in_service::Float64
    in_travel::Float64
    in_waiting::Float64
    total_distance::Float64
end

function update_times_waiting(solution_times::SolutionTimes,
                              req::Request, act::Action,
                              waiting_time::Float64)
    # Waiting time based on the request
    if act.operation_type == 1
        tws = [_tw.soft_range for _tw in
               req.pickup_time_windows]
    elseif act.operation_type == 2
        tws = [_tw.soft_range for _tw in
               req.delivery_time_windows]
    end
    tw = get_closest_tw(tws, solution_times.current)
    waiting_time = max(0, max(waiting_time, tw.lb - solution_times.current))
    solution_times.in_waiting += waiting_time
    solution_times.current += waiting_time
    # Scheduled start time
    act.scheduled_start_time = solution_times.current
end

function update_times_service(solution_times::SolutionTimes,
                              req::Request, operation_type::Int)
    if operation_type == 1
        service_time = req.pickup_service_time
    elseif operation_type == 2
        service_time = req.delivery_service_time
    end
    solution_times.current += service_time
    solution_times.in_service += service_time
end

function compute_route_cost(solution_times::SolutionTimes,
                           v_set::HomogeneousVehicleSet)
    return (
        + v_set.cost_periods[1].travel_distance_unit_cost * solution_times.total_distance
        + v_set.cost_periods[1].travel_time_unit_cost * solution_times.in_travel
        + v_set.cost_periods[1].service_time_unit_cost * solution_times.in_service
        + v_set.cost_periods[1].waiting_time_unit_cost * solution_times.in_waiting
        + v_set.cost_periods[1].fixed_cost
    )
end

function compute_cost_and_scheduled_times(sol::RvrpSolution,
                                          data::RvrpInstance,
                                          computed_data::RvrpComputedData)

    cost = 0.0
    distances = data.travel_specifications[1].travel_distance_matrix
    times = data.travel_specifications[1].travel_time_matrix
    has_distances = !isempty(distances)
    has_times = !isempty(times)

    for r in sol.routes
        v_set = data.vehicle_sets[computed_data.vehicle_set_id_2_index[r.vehicle_set_id]]
        solution_times = SolutionTimes(v_set.work_periods[1].soft_range.lb,
                                       0.0, 0.0, 0.0, 0.0)
        prev_act = r.sequence[1]

        for act in r.sequence[2:end]

            prev_loc_idx = computed_data.location_id_2_index[prev_act.location_id]
            curr_loc_idx = computed_data.location_id_2_index[act.location_id]

            # Distance
            if has_distances
                solution_times.total_distance += distances[prev_loc_idx,curr_loc_idx]
            end

            # Travel time
            if has_times
                travel_time = times[prev_loc_idx,curr_loc_idx]
                solution_times.current += travel_time
                solution_times.in_travel += travel_time
            end

            # Waiting time based on the location
            tw = get_closest_tw(data.locations[prev_loc_idx].opening_time_windows,
                                solution_times.current)
            waiting_time = max(0, tw.lb - solution_times.current)

            req_id = act.request_id
            # If action is from a request
            if haskey(computed_data.request_id_2_index, req_id)
                req = data.requests[computed_data.request_id_2_index[req_id]]
                update_times_waiting(solution_times, req, act, waiting_time)
                update_times_service(solution_times, req, act.operation_type)
            end

            prev_act = act
        end
        # Update costs
        cost += compute_route_cost(solution_times, v_set)
    end
    sol.cost = cost
end

function supported_features(::Type{MockSolver})
    features = BitSet()

    # Location based features  #
    union!(features, HAS_OPENING_TIME_WINDOWS)

    # Product based features #
    union!(features, HAS_PRODUCT_CAPACITY_CONSUMPTIONS)
    union!(features, HAS_MULTIPLE_PRODUCT_CAPACITY_CONSUMPTIONS)

    # Request based features
    union!(features, HAS_SHIPMENT_REQUESTS)
    union!(features, HAS_PICKUPONLY_REQUESTS)
    union!(features, HAS_DELIVERYONLY_REQUESTS)
    union!(features, HAS_PICKUP_TIME_WINDOWS)
    union!(features, HAS_DELIVERY_TIME_WINDOWS)

    # VehicleCategory based features
    union!(features, HAS_VEHICLE_CAPACITIES)
    union!(features, HAS_MULTIPLE_VEHICLE_CAPACITIES)

    # HomogeneousVehicleSet based features
    union!(features, HAS_TRAVEL_TIME_UNIT_COST)
    union!(features, HAS_SERVICE_TIME_UNIT_COST)
    union!(features, HAS_WAITING_TIME_UNIT_COST)
    union!(features, HAS_TRAVEL_DISTANCE_UNIT_COST)
    union!(features, HAS_MAX_NB_VEHICLES)
    union!(features, HAS_WORKING_TIME_WINDOW)
    union!(features, HAS_FIXED_COST_PER_VEHICLE)
    union!(features, HAS_ARRIVAL_DIFFERENT_FROM_DEPARTURE)

    # Instance based features
    union!(features, HAS_MULTIPLE_VEHICLE_CATEGORIES)
    union!(features, HAS_MULTIPLE_VEHICLE_SETS)

    return [features]
end


println("Loading Libraries onto main node ...")
include("post_process_imports.jl")
println("Finished loading Libraries onto main node ...")

using Distributed
addprocs(20)

@everywhere include("post_process_defs.jl")
include("../Util/Util.jl")

if length(ARGS) > 0
  RESULTS_DIR = ARGS[1]
  AWS_SIM_BUCKET = ARGS[2]
else
  RESULTS_DIR = "results/test-sims"
  AWS_SIM_BUCKET = "s3://cytof-repfam/test-sims"
end

# Collecting paths to output
paths_to_output = ["$(root)/output.bson"
                   for (root, dirs, files) in walkdir(RESULTS_DIR)
                   if "output.bson" in files]

# Post processing
println("Doing post processing...")
@everywhere function makeplots(path_to_output)
  println("Processing: $(path_to_output)")
  pathdir = getpath(path_to_output)
  path_to_simdat = "$(pathdir)/simdat.bson"
  post_process(path_to_output, path_to_simdat=path_to_simdat)
end

println("Make graphs in parallel ...")
status = pmap(makeplots, paths_to_output, on_error=identity)

println("Send results to S3 ...")
Util.s3sync(RESULTS_DIR, AWS_SIM_BUCKET)

println("Success / Failure status: ")
simsucceeded(x) = (x == nothing)
simfailed(x) = !simsucceeded(x)
numsuccess = count(simsucceeded, status)
println("Number of simulations successfully processed: $(numsuccess)")
failures_indices = findall(simfailed, status)
println("Simulations unsuccessfully processed:")
foreach(i -> println(paths_to_output[i]), failures_indices)

println("DONE!")

#= TEST
path_to_output = "results/test-sims/KMCMC2/z1/scale0/output.bson"
path_to_simdat = "results/test-sims/KMCMC2/z1/scale0/simdat.bson"
include("post_process_defs.jl")
post_process(path_to_output, path_to_simdat=path_to_simdat)
=#