"""
`module  JAC.AtomicState`
    ... a submodel of JAC that contains all methods to set-up and process atomic representations.
"""
module AtomicState

    ## using Interact
    using ..Basics, ..Radial, ..ManyElectron, ..Nuclear

    export  MeanFieldSettings, MeanFieldBasis, CiSettings, CiExpansion, RasSettings, RasStep, RasExpansion,
            GreenSettings, GreenChannel, GreenExpansion, Representation

    """
    `abstract type AtomicState.AbstractRepresentationType`
        ... defines an abstract type and a number of data types to work with and distinguish different atomic representations; see also:

        + struct MeanFieldBasis       ... to represent (and generate) a mean-field basis and, especially, a set of (mean-field) orbitals.
        + struct CiExpansion          ... to represent (and generate) a configuration-interaction representation.
        + struct RasExpansion         ... to represent (and generate) a restricted active-space representation.
        + struct GreenFunction        ... to represent (and generate) an approximate (many-electron) Green functions.
    """
    abstract type  AbstractRepresentationType                           end

    """
    `struct  AtomicState.MeanFieldSettings`
        ... a struct for defining the settings for a mean-field basis (orbital) representation.

        + scField           ::AbstractScField
            ... Specify the (mean) self-consistent field as DFSField() or HSField(); note that not all AbstractScField's
                allowed here.
    """
    struct  MeanFieldSettings
        scField              ::AbstractScField
    end

    """
    `AtomicState.MeanFieldSettings()`  ... constructor for setting the default values.
    """
    function MeanFieldSettings()
    	MeanFieldSettings(DFSField())
    end

    # `Base.show(io::IO, settings::MeanFieldSettings)`  ... prepares a proper printout of the settings::MeanFieldSettings.
    function Base.show(io::IO, settings::MeanFieldSettings)
    	  println(io, "scField:                  $(settings.scField)  ")
    end

    """
    `struct  AtomicState.MeanFieldBasis  <:  AbstractRepresentationType`
        ... a struct to represent (and generate) a mean-field orbital basis.

        + settings         ::AtomicState.MeanFieldSettings      ... Settings for the given mean-field orbital basis
    """
    struct   MeanFieldBasis  <:  AbstractRepresentationType
        settings           ::AtomicState.MeanFieldSettings
    end

    # `Base.string(basis::MeanFieldBasis)`  ... provides a String notation for the variable basis::MeanFieldBasi.
    function Base.string(basis::MeanFieldBasis)
        if !(basis.settings.scField  in [DFSField(), HSField()])
            error("A MeanFieldBasis presently supports only a DFSField() or HSField() but received:  $(basis.settings.scField)")
        end
        #
        sa = "Mean-field orbital basis for a $(basis.settings.scField) SCF field:"
        return( sa )
    end

    # `Base.show(io::IO, basis::MeanFieldBasis)`  ... prepares a proper printout of the basis::MeanFieldBasis.
    function Base.show(io::IO, basis::MeanFieldBasis)
        sa = Base.string(basis);       print(io, sa, "\n")
    end

    ### RAS: Restricted-Active-Space expansions ##################################################################################
    """
    `struct  AtomicState.RasSettings`
        ... a struct for defining the settings for a restricted active-space computations.

        + levelsScf            ::Array{Int64,1}         ... Levels on which the optimization need to be carried out.
        + maxIterationsScf     ::Int64                  ... maximum number of SCF iterations in each RAS step.
        + accuracyScf          ::Float64                ... convergence criterion for the SCF field.

    	+ eeInteractionCI      ::AbstractEeInteraction  ... logical flag to include Breit interactions.
        + levelSelectionCI     ::LevelSelection         ... Specifies the selected levels, if any.
    """
    struct  RasSettings
        levelsScf              ::Array{Int64,1}
        maxIterationsScf       ::Int64
        accuracyScf            ::Float64
    	eeInteractionCI        ::AbstractEeInteraction
    	levelSelectionCI       ::LevelSelection
    end

    """
    `AtomicState.RasSettings()`  ... constructor for setting the default values.
    """
    function RasSettings()
    	RasSettings(Int64[1], 24, 1.0e-6, false, LevelSelection() )
    end

    # `Base.show(io::IO, settings::RasSettings)`  ... prepares a proper printout of the settings::RasSettings.
    function Base.show(io::IO, settings::RasSettings)
    	  println(io, "levelsScf:            $(settings.levelsScf)  ")
    	  println(io, "maxIterationsScf:     $(settings.maxIterationsScf)  ")
    	  println(io, "accuracyScf:          $(settings.accuracyScf)  ")
    	  println(io, "eeInteractionCI:      $(settings.eeInteractionCI)  ")
    	  println(io, "levelSelectionCI:     $(settings.levelSelectionCI)  ")
    end

    """
    `struct  AtomicState.RasStep`
        ... specifies an individual step of a (relativistic) restricted active space computation for a set of levels. This struct
            comprises all information to generate the orbital basis and to perform the associated SCF and multiplet computations for a
            selected number of levels.

        + seFrom            ::Array{Shell,1}        ... Single-excitations from shells   [sh_1, sh_2, ...]
        + seTo              ::Array{Shell,1}        ... Single-excitations to shells  [sh_1, sh_2, ...]
        + deFrom            ::Array{Shell,1}        ... Double-excitations from shells   [sh_1, sh_2, ...]
        + deTo              ::Array{Shell,1}        ... Double-excitations to shells  [sh_1, sh_2, ...]
        + teFrom            ::Array{Shell,1}        ... Triple-excitations from shells   [sh_1, sh_2, ...]
        + teTo              ::Array{Shell,1}        ... Triple-excitations to shells  [sh_1, sh_2, ...]
        + qeFrom            ::Array{Shell,1}        ... Quadrupole-excitations from shells   [sh_1, sh_2, ...]
        + qeTo              ::Array{Shell,1}        ... Quadrupole-excitations to shells  [sh_1, sh_2, ...]
        + frozenShells      ::Array{Shell,1}        ... List of shells that are kept 'frozen' in this step.
        + constraints       ::Array{String,1}       ... List of Strings to define 'constraints/restrictions' to the generated CSF basis.
    """
    struct  RasStep
        seFrom              ::Array{Shell,1}
        seTo                ::Array{Shell,1}
        deFrom              ::Array{Shell,1}
        deTo                ::Array{Shell,1}
        teFrom              ::Array{Shell,1}
        teTo                ::Array{Shell,1}
        qeFrom              ::Array{Shell,1}
        qeTo                ::Array{Shell,1}
        frozenShells        ::Array{Shell,1}
        constraints         ::Array{String,1}
    end

    """
    `AtomicState.RasStep()`  ... constructor for an 'empty' instance of a variable::AtomicState.RasStep
    """
    function RasStep()
        RasStep(Shell[], Shell[], Shell[], Shell[], Shell[], Shell[], Shell[], Shell[],    Shell[], String[])
    end

    """
    `AtomicState.RasStep(rasStep::AtomicState.RasStep;`

                        seFrom::Array{Shell,1}=Shell[], seTo::Array{Shell,1}=Shell[],
                        deFrom::Array{Shell,1}=Shell[], deTo::Array{Shell,1}=Shell[],
                        teFrom::Array{Shell,1}=Shell[], teTo::Array{Shell,1}=Shell[],
                        qeFrom::Array{Shell,1}=Shell[], qeTo::Array{Shell,1}=Shell[],
                        frozen::Array{Shell,1}=Shell[], constraints::Array{String,1}=String[]

        ... constructor for modifying the given rasStep by specifying all excitations, frozen shells and constraints optionally.
    """
    function RasStep(rasStep::AtomicState.RasStep;
                     seFrom::Array{Shell,1}=Shell[], seTo::Array{Shell,1}=Shell[],
                     deFrom::Array{Shell,1}=Shell[], deTo::Array{Shell,1}=Shell[],
                     teFrom::Array{Shell,1}=Shell[], teTo::Array{Shell,1}=Shell[],
                     qeFrom::Array{Shell,1}=Shell[], qeTo::Array{Shell,1}=Shell[],
                     frozen::Array{Shell,1}=Shell[], constraints::Array{String,1}=String[])
        if  seFrom == Shell[]   sxFrom = rasStep.seFrom   else      sxFrom = seFrom      end
        if  seTo   == Shell[]   sxTo   = rasStep.seTo     else      sxTo   = seTo        end
        if  deFrom == Shell[]   dxFrom = rasStep.deFrom   else      dxFrom = deFrom      end
        if  deTo   == Shell[]   dxTo   = rasStep.deTo     else      dxTo   = deTo        end
        if  teFrom == Shell[]   txFrom = rasStep.teFrom   else      txFrom = teFrom      end
        if  teTo   == Shell[]   txTo   = rasStep.teTo     else      txTo   = teTo        end
        if  qeFrom == Shell[]   qxFrom = rasStep.qeFrom   else      qxFrom = qeFrom      end
        if  qeTo   == Shell[]   qxTo   = rasStep.qeTo     else      qxTo   = qeTo        end
        if  frozen == Shell[]        frozx  = rasStep.frozenShells   else      frozx  = frozen             end
        if  constraints ==String[]   consx  = rasStep.constraints    else      consx  = constraints        end

        RasStep( sxFrom, sxTo, dxFrom, dxTo, txFrom, txTo, qxFrom, qxTo, frozx, consx)
    end

    # `Base.string(step::AtomicState.RasStep)`  ... provides a String notation for the variable step::AtomicState.RasStep.
    function Base.string(step::AtomicState.RasStep)
        sa = "\nCI or RAS step with $(length(step.frozenShells)) (explicitly) frozen shell(s): $(step.frozenShells)  ... and virtual excitations"
        return( sa )
    end

    # `Base.show(io::IO, step::AtomicState.RasStep)`  ... prepares a proper printout of the (individual step of computations) step::AtomicState.RasStep.
    function Base.show(io::IO, step::AtomicState.RasStep)
        sa = Base.string(step);                 print(io, sa, "\n")
        if  length(step.seFrom) > 0
           sa = "   Singles from:          { ";      for  sh in step.seFrom   sa = sa * string(sh) * ", "  end
           sa = sa[1:end-2] * " }   ... to { ";      for  sh in step.seTo     sa = sa * string(sh) * ", "  end;
           sa = sa[1:end-2] * " }";            print(io, sa, "\n")
        end
        if  length(step.deFrom) > 0
           sa = "   Doubles from:          { ";      for  sh in step.deFrom   sa = sa * string(sh) * ", "  end
           sa = sa[1:end-2] * " }   ... to { ";      for  sh in step.deTo     sa = sa * string(sh) * ", "  end;
           sa = sa[1:end-2] * " }";            print(io, sa, "\n")
        end
        if  length(step.teFrom) > 0
           sa = "   Triples from:          { ";      for  sh in step.teFrom   sa = sa * string(sh) * ", "  end
           sa = sa[1:end-2] * " }   ... to { ";      for  sh in step.teTo     sa = sa * string(sh) * ", "  end;
           sa = sa[1:end-2] * " }";            print(io, sa, "\n")
        end
        if  length(step.qeFrom) > 0
           sa = "   Quadruples from:       { ";      for  sh in step.qeFrom   sa = sa * string(sh) * ", "  end
           sa = sa[1:end-2] * " }   ... to { ";      for  sh in step.qeTo     sa = sa * string(sh) * ", "  end;
           sa = sa[1:end-2] * " }";            print(io, sa, "\n")
        end
    end

    """
    `struct  AtomicState.RasExpansion    <:  AbstractRepresentationType`
        ... a struct to represent (and generate) a restricted active-space representation.

        + symmetry         ::LevelSymmetry                  ... Symmetry of the levels/CSF in the many-electron basis.
        + NoElectrons      ::Int64                          ... Number of electrons.
        + steps            ::Array{AtomicState.RasStep,1}   ... List of SCF steps that are to be done in this model computation.
        + settings         ::AtomicState.RasSettings        ... Settings for the given RAS computation
    """
    struct         RasExpansion    <:  AbstractRepresentationType
        symmetry           ::LevelSymmetry
        NoElectrons        ::Int64
        steps              ::Array{AtomicState.RasStep,1}
        settings           ::AtomicState.RasSettings
     end

    """
    `AtomicState.RasExpansion()`  ... constructor for an 'empty' instance of the a variable::AtomicState.RasExpansion
    """
    function RasExpansion()
        RasExpansion(Basics.LevelSymmetry(0, Basics.plus), 0, 0, AtomicState.RasStep[], AtomicState.RasSettings())
    end

    # `Base.string(expansion::RasExpansion)`  ... provides a String notation for the variable expansion::RasExpansion.
    function Base.string(expansion::RasExpansion)
        sa = "RAS expansion for symmétry $(expansion.symmetry) and with $(length(expansion.steps)) steps:"
        return( sa )
    end

    # `Base.show(io::IO, expansion::RasExpansion)`  ... prepares a proper printout of the (individual step of computations) expansion::RasExpansion.
    function Base.show(io::IO, expansion::RasExpansion)
        sa = Base.string(expansion);       print(io, sa, "\n")
        println(io, "$(expansion.steps)  ")
        println(io, "... and the current settings:")
        println(io, "$(expansion.settings)  ")
    end

    ### CI: Configuration Interaction expansions ##################################################################################
    """
    `struct  AtomicState.CiSettings`
        ... a struct for defining the settings for a configuration-interaction (CI) expansion.

    	+ eeInteractionCI      ::AbstractEeInteraction   ... logical flag to include Breit interactions.
        + levelSelectionCI     ::LevelSelection          ... Specifies the selected levels, if any.
    """
    struct  CiSettings
    	eeInteractionCI        ::AbstractEeInteraction
    	levelSelectionCI       ::LevelSelection
    end

    """
    `AtomicState.CiSettings()`  ... constructor for setting the default values.
    """
    function CiSettings()
    	CiSettings(CoulombInteraction(), LevelSelection() )
    end

    """
    `AtomicState.CiSettings(settings::AtomicState.CiSettings;`

            eeInteractionCI::Union{Nothing,AbstractEeInteraction}=nothing,    levelSelectionCI::Union{Nothing,LevelSelection}=nothing)

        ... constructor for modifying the given CiSettings by 'overwriting' the explicitly selected parameters.
    """
    function CiSettings(settings::AtomicState.CiSettings;
        eeInteractionCI::Union{Nothing,AbstractEeInteraction}=nothing,        levelSelectionCI::Union{Nothing,LevelSelection}=nothing)

        if  eeInteractionCI     == nothing   eeInteractionCIx      = settings.eeInteractionCI       else   eeInteractionCIx      = eeInteractionCI      end
        if  levelSelectionCI    == nothing   levelSelectionCIx     = settings.levelSelectionCI      else   levelSelectionCIx     = levelSelectionCI     end

        CiSettings( eeInteractionCIx, levelSelectionCIx)
    end

    # `Base.show(io::IO, settings::CiSettings)`  ... prepares a proper printout of the settings::CiSettings.
    function Base.show(io::IO, settings::CiSettings)
    	  println(io, "eeInteractionCI:          $(settings.eeInteractionCI)  ")
    	  println(io, "levelSelectionCI:         $(settings.levelSelectionCI)  ")
    end

    """
    `struct  AtomicState.CiExpansion  <:  AbstractRepresentationType`
        ... a struct to represent (and generate) a configuration-interaction representation.

        + applyOrbitals    ::Dict{Subshell, Orbital}
        + excitations      ::AtomicState.RasStep            ... Excitations beyond refConfigs.
        + settings         ::AtomicState.CiSettings         ... Settings for the given CI expansion
    """
    struct         CiExpansion     <:  AbstractRepresentationType
        applyOrbitals      ::Dict{Subshell, Orbital}
        excitations        ::AtomicState.RasStep
        settings           ::AtomicState.CiSettings
    end

    # `Base.string(expansion::CiExpansion)`  ... provides a String notation for the variable expansion::CiExpansion.
    function Base.string(expansion::CiExpansion)
        sa = "CI expansion with (additional) excitations:"
        return( sa )
    end

    # `Base.show(io::IO, expansion::CiExpansion)`  ... prepares a proper printout of the (individual step of computations) expansion::CiExpansion.
    function Base.show(io::IO, expansion::CiExpansion)
        sa = Base.string(expansion);       print(io, sa, "\n")
        println(io, "$(expansion.excitations)  ")
        println(io, "... and the current settings:")
        println(io, "$(expansion.settings)  ")
    end

    ### Green function representation ##################################################################################

    """
    `abstract type AtomicState.AbstractGreenApproach`
        ... defines an abstract and a number of singleton types for approximating a many-electron Green
            function expansion for calculating second-order processes.

      + struct SingleCSFwithoutCI
        ... to approximate the many-electron multiplets (gMultiplet) for every chosen level symmetry by dealing with each CSF
            independently, and without any configuration interaction. This is a fast but also very rough approximation.

      + struct CoreSpaceCI
        ... to approximate the many-electron multiplets (gMultiplet) by taking the electron-electron interaction between the
            bound-state orbitals into account.

      + struct DampedSpaceCI
        ... to approximate the many-electron multiplets (gMultiplet) by taking the electron-electron interaction for all, the
            bound and free-electron, orbitals into account but by including a damping factor e^{tau*r}, tau > 0 into the
            electron densities rho_ab (r) --> rho_ab (r) * e^{tau*r}
    """
    abstract type  AbstractGreenApproach                                  end
    struct         SingleCSFwithoutCI  <:  AtomicState.AbstractGreenApproach   end
    struct         CoreSpaceCI         <:  AtomicState.AbstractGreenApproach   end
    struct         DampedSpaceCI       <:  AtomicState.AbstractGreenApproach   end

    """
    `struct  AtomicState.GreenSettings`
        ... defines a type for defining the details and parameters of the approximate Green (function) expansion.

        + nMax                     ::Int64            ... maximum principal quantum numbers of (single-electron)
                                                          excitations that are to be included into the representation.
        + lValues                  ::Array{Int64,1}   ... List of (non-relativistic) orbital angular momenta for which
                                                          (single-electron) excitations are to be included.
        + dampingTau               ::Float64          ... factor tau (> 0.) that is used to 'damp' the one- and two-electron
                                                          interactions strength: exp( - tau * r)
        + printBefore              ::Bool             ... True if a short overview is to be printed before.
        + levelSelection           ::LevelSelection   ... Specifies the selected levels, if any.
    """
    struct GreenSettings
        nMax                       ::Int64
        lValues                    ::Array{Int64,1}
        dampingTau                 ::Float64
        printBefore                ::Bool
        levelSelection             ::LevelSelection
    end

    """
    `AtomicState.GreenSettings()`  ... constructor for an `empty` instance of AtomicState.GreenSettings.
    """
    function GreenSettings()
        Settings( 0, Int64[], 0., false, LevelSelection() )
    end

    # `Base.show(io::IO, settings::AtomicState.GreenSettings)`  ... prepares a proper printout of the variable settings::AtomicState.GreenSettings.
    function Base.show(io::IO, settings::AtomicState.GreenSettings)
        println(io, "nMax:                     $(settings.nMax)  ")
        println(io, "lValues:                  $(settings.lValues)  ")
        println(io, "dampingTau:               $(settings.dampingTau)  ")
        println(io, "printBefore:              $(settings.printBefore)  ")
        println(io, "levelSelection:           $(settings.levelSelection)  ")
    end

    """
    `struct  AtomicState.GreenChannel`
        ... defines a type for a single symmetry channel of an (approximate) Green (function) expansion.

        + symmetry          ::LevelSymmetry    ... Level symmetry of this part of the representation.
        + gMultiplet        ::Multiplet        ... Multiplet of (scattering) levels of this symmetry.
    """
    struct GreenChannel
        symmetry            ::LevelSymmetry
        gMultiplet          ::Multiplet
    end

    """
    `AtomicState.GreenChannel()`  ... constructor for an `empty` instance of AtomicState.GreenChannel.
    """
    function GreenChannel()
        GreenChannel( LevelSymmetry(0, Basics.plus), ManyElectron.Multiplet)
    end

    # `Base.show(io::IO, channel::AtomicState.GreenChannel)`  ... prepares a proper printout of the variable channel::AtomicState.GreenChannel.
    function Base.show(io::IO, channel::AtomicState.GreenChannel)
        println(io, "symmetry:                $(channel.symmetry)  ")
        println(io, "gMultiplet:              $(channel.gMultiplet)  ")
    end

    """
    `struct  AtomicState.GreenExpansion  <:  AbstractRepresentationType`
        ... defines a type to keep an (approximate) Green (function) expansion that is associated with a given set of reference
            configurations.

        + approach          ::AtomicState.AbstractGreenApproach  ... Approach used to approximate the representation.
        + excitationScheme  ::Basics.AbstractExcitationScheme    ... Applied excitation scheme w.r.t. refConfigs.
        + levelSymmetries   ::Array{LevelSymmetry,1}             ... Total symmetries J^P to be included into Green expansion.
        + NoElectrons       ::Int64                              ... Number of electrons.
        + settings          ::AtomicState.GreenSettings          ... settings for the Green (function) expansion.
    """
    struct GreenExpansion  <:  AbstractRepresentationType
        approach            ::AtomicState.AbstractGreenApproach
        excitationScheme    ::Basics.AbstractExcitationScheme
        levelSymmetries     ::Array{LevelSymmetry,1}
        NoElectrons         ::Int64
        settings            ::AtomicState.GreenSettings
    end

    """
    `AtomicState.GreenExpansion()`  ... constructor for an `empty` instance of AtomicState.GreenExpansion.
    """
    function GreenExpansion()
        GreenExpansion( AtomicState.SingleCSFwithoutCI(), Basics.NoExcitationScheme(), LevelSymmetry[], 0, AtomicState.GreenSettings())
    end

    # `Base.string(expansion::GreenExpansion)`  ... provides a String notation for the variable expansion::GreenExpansion.
    function Base.string(expansion::GreenExpansion)
        sa = "Green (function) expansion in $(expansion.approach) approach and for excitation scheme  $(expansion.excitationScheme)," *
             "\nincluding (Green function channels with) symmetries $(expansion.levelSymmetries):"
        return( sa )
    end

    # `Base.show(io::IO, expansion::GreenExpansion)`  ... prepares a proper printout of the (individual step of computations) expansion::GreenExpansion.
    function Base.show(io::IO, expansion::GreenExpansion)
        sa = Base.string(expansion);       print(io, sa, "\n")
        println(io, "... and the current settings:")
        println(io, "$(expansion.settings)  ")
    end

    ### Representation ##################################################################################
    """
    `struct  AtomicState.Representation`
        ... a struct for defining an atomic state representation. Such representations often refer to approximate wave function approximations of
            one or several levels but may concern also a mean-field basis (for some multiplet of some given configurations) or Green functions,
            etc.

        + name             ::String                      ... to assign a name to the given model.
        + nuclearModel     ::Nuclear.Model               ... Model, charge and parameters of the nucleus.
        + grid             ::Radial.Grid                 ... The radial grid to be used for the computation.
        + refConfigs       ::Array{Configuration,1}      ... List of references configurations, at least 1.
        + repType          ::AbstractRepresentationType  ... Specifies the particular representation.
    """
    struct  Representation
        name               ::String
        nuclearModel       ::Nuclear.Model
        grid               ::Radial.Grid
        refConfigs         ::Array{Configuration,1}
        repType            ::AbstractRepresentationType
    end

    """
    `AtomicState.Representation()`  ... constructor for an 'empty' instance of the a variable::AtomicState.Representation
    """
    function Representation()
        Representation("", Nuclear.Model(1.0), Radial.Grid(), ManyElectron.Configuration[], CiExpansion())
    end

    """
    `AtomicState.Representation( ... example for the generation of a mean-field basis)`

            name        = "Oxygen 1s^2 2s^2 2p^4 ground configuration"
            grid        = Radial.Grid(true)
            nuclearM    = Nuclear.Model(8.)
            refConfigs  = [Configuration("[He] 2s^2 2p^4")]
            mfSettings  = MeanFieldSettings()
            Representation(name, nuclearM, grid, refConfigs, MeanFieldBasis(mfSettings) )

    `AtomicState.Representation( ... example for the computation of a configuration-interaction (CI) expansion)`

            name        = "Oxygen 1s^2 2s^2 2p^4 ground configuration"
            grid        = Radial.Grid(true)
            nuclearM    = Nuclear.Model(8.)
            refConfigs  = [Configuration("[He] 2s^2 2p^4")]
            orbitals    = wb["mean-field basis"].orbitals #   get a proper set of orbitals
            ciSettings  = CiSettings(true, false, Int64[], false, LevelSymmetry[] )
            from        = [Shell("2s")]
            to          = [Shell("2s"), Shell("2p")]
            excitations = RasStep(RasStep(), seFrom=from, seTo=to, deFrom=from, deTo=to, frozen=[Shell("1s")])
            Representation(name, nuclearM, grid, refConfigs, CiExpansion(orbitals, excitations, ciSettings) )

    `AtomicState.Representation( ... example for the computation of a restricted-active-space (RAS) expansion)`

            name        = "Beryllium 1s^2 2s^2 ^1S_0 ground state"
            refConfigs  = [Configuration("[He] 2s^2")]
            rasSettings = RasSettings([1], 24, 1.0e-6, CoulombInteraction(), true, [1,2,3] )
            from        = [Shell("2s")]

            frozen      = [Shell("1s")]
            to          = [Shell("2s"), Shell("2p")]
            step1       = RasStep(RasStep(), seFrom=from, seTo=deepcopy(to), deFrom=from, deTo=deepcopy(to), frozen=deepcopy(frozen))

            append!(frozen, [Shell("2s"), Shell("2p")])
            append!(to,     [Shell("3s"), Shell("3p"), Shell("3d")])
            step2       = RasStep(step1; seTo=deepcopy(to), deTo=deepcopy(to), frozen=deepcopy(frozen))

            append!(frozen, [Shell("3s"), Shell("3p"), Shell("3d")])
            append!(to,     [Shell("4s"), Shell("4p"), Shell("4d"), Shell("4f")])
            step3       = RasStep(step2, seTo=deepcopy(to), deTo=deepcopy(to), frozen=deepcopy(frozen))

            Representation(name, Nuclear.Model(4.), Radial.Grid(true), refConfigs,
                           RasExpansion(LevelSymmetry(0, Basics.plus), 4, [step1, step2, step3], rasSettings) )

    `AtomicState.Representation( ... example for the computation of Green(function) expansion)`

            name            = "Lithium 1s^2 2s ground configuration"
            refConfigs      = [Configuration("[He] 2s")]
            levelSymmetries = [LevelSymmetry(1//2, Basics.plus), LevelSymmetry(3//2, Basics.plus)]
            greenSettings   = GreenSettings(5, [0, 1, 2], 0.01, true, false, Int64[])
            Representation(name, Nuclear.Model(8.), Radial.Grid(true), refConfigs,
                           GreenExpansion( AtomicState.DampedSpaceCI(), Basics.DeExciteSingleElectron(), levelSymmetries, 3, greenSettings) )

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

    # `Base.string(rep::Representation)`  ... provides a String notation for the variable rep::AtomicState.Representation
    function Base.string(rep::Representation)
        sa = "Atomic representation:   $(rep.name) for Z = $(rep.nuclearModel.Z) and with reference configurations: \n   "
        for  refConfig  in  rep.refConfigs     sa = sa * string(refConfig) * ",  "     end
        return( sa )
    end

    # `Base.show(io::IO, rep::Representation)`  ... prepares a printout of rep::Representation.
    function Base.show(io::IO, rep::Representation)
        sa = Base.string(rep);            print(io, sa, "\n")
        println(io, "representation type:   $(rep.repType)  ")
        println(io, "nuclearModel:          $(rep.nuclearModel)  ")
        println(io, "grid:                  $(rep.grid)  ")
    end

end # module


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# Description
#
#   Helpers for SGP4 algorithm.
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

export sgp4

"""
    sgp4(Δt, args...)

Function that initialize the SGP4 structure and propagate the orbit until the time Δt.

# Returns

* The position vector [km].
* The velocity vector [km/s].
* The SGP4 structure (see `SGP4_Structure`).

"""
function sgp4(Δt, args...)
    sgp4d = sgp4_init(args...)
    r,v   = sgp4!(sgp4d, Δt)
    return r,v,sgp4d
end


using Gtk, GLWindow, GLAbstraction, Reactive, GeometryTypes, Colors, GLVisualize
using GtkReactive
using Gtk.GConstants, ModernGL

mutable struct GtkContext <: GLWindow.AbstractContext
    window::Gtk.GLArea
    framebuffer::GLWindow.GLFramebuffer
end
function make_context_current(screen::Screen)
    gl_area = screen.glcontext.window
    Gtk.make_current(gl_area)
end
GLWindow.isopen(x::Gtk.GLArea) = true

global screen = Ref{Screen}()
function init_screen(gl_area, resolution, mesh_color)
    Gtk.make_current(gl_area)
    gtk_context = GtkContext(gl_area, GLWindow.GLFramebuffer(Signal(resolution)))
    window_area = Signal(SimpleRectangle(0, 0, resolution...))
    signals = Dict(
        :mouse_button_released => Reactive.Signal(0),
        :mouse_buttons_pressed => Reactive.Signal(Set(Int[])),
        :scroll => Reactive.Signal(Vec(0.0, 0.0)),
        :buttons_pressed => Reactive.Signal(Set(Int[])),
        :window_size => Reactive.Signal(Vec(resolution...)),
        :window_area => window_area,
        :cursor_position => Reactive.Signal(Vec(0.0,0.0)),
        :mouseinside => Reactive.Signal(true),
        :mouse_button_down => Reactive.Signal(0),
        :mouseposition => Reactive.Signal(Vec(0.0, 0.0)),
        :framebuffer_size => Reactive.Signal(Vec(resolution...)),
        :button_down => Reactive.Signal(0),
        :button_released => Reactive.Signal(0),
        :window_open => Reactive.Signal(true),
        :keyboard_buttons => Reactive.Signal((0,0,0,0)),
        :mouse2id => Signal(GLWindow.SelectionID{Int}(-1, -1))
    )
    screen[] = Screen(
        Symbol("GLVisualize"), window_area, nothing,
        Screen[], signals,
        (), false, true, RGBA(1f0,1f0,1f0,1f0), (0f0, RGBA(0f0,0f0,0f0,0f0)),
        Dict{Symbol, Any}(),
        gtk_context
    )

    GLVisualize.add_screen(screen[])
    timesignal = loop(linspace(0f0,1f0,360))
    rotation_angle  = const_lift(*, timesignal, 2f0*pi)
    start_rotation  = Signal(rotationmatrix_x(deg2rad(90f0)))
    rotation        = map(rotationmatrix_y, rotation_angle)
    final_rotation  = map(*, start_rotation, rotation)
    foreach(final_rotation) do x
        # render a frame each time rotation updates
        Gtk.queue_render(gl_area)
        return
    end
    _view(visualize(loadasset("cat.obj"), color = mesh_color, model = final_rotation), screen[])
    # renderloop(screen[])

end

function render_gtk(window, gtk_area)
    !isopen(window) && return
    fb = GLWindow.framebuffer(window)
    wh = GeometryTypes.widths(window)
    resize!(fb, wh)
    w, h = wh
    #prepare for geometry in need of anti aliasing
    glBindFramebuffer(GL_FRAMEBUFFER, fb.id[1]) # color framebuffer
    glDrawBuffers(2, [GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1])
    # setup stencil and backgrounds
    glEnable(GL_STENCIL_TEST)
    glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE)
    glStencilMask(0xff)
    glClearStencil(0)
    glClearColor(0,0,0,0)
    glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT)
    glEnable(GL_SCISSOR_TEST)
    GLWindow.setup_window(window, false)
    glDisable(GL_SCISSOR_TEST)
    glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE)
    # deactivate stencil write
    glEnable(GL_STENCIL_TEST)
    glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE)
    glStencilMask(0x00)
    GLAbstraction.render(window, true)
    glDisable(GL_STENCIL_TEST)

    # transfer color to luma buffer and apply fxaa
    glBindFramebuffer(GL_FRAMEBUFFER, fb.id[2]) # luma framebuffer
    glDrawBuffer(GL_COLOR_ATTACHMENT0)
    glClearColor(0,0,0,0)
    glClear(GL_COLOR_BUFFER_BIT)
    glViewport(0, 0, w, h)
    GLAbstraction.render(fb.postprocess[1]) # add luma and preprocess

    glBindFramebuffer(GL_FRAMEBUFFER, fb.id[1]) # transfer to non fxaa framebuffer
    glDrawBuffer(GL_COLOR_ATTACHMENT0)
    GLAbstraction.render(fb.postprocess[2]) # copy with fxaa postprocess

    #prepare for non anti aliased pass
    glDrawBuffers(2, [GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1])

    glEnable(GL_STENCIL_TEST)
    glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE)
    glStencilMask(0x00)
    GLAbstraction.render(window, false)
    glDisable(GL_STENCIL_TEST)
    # draw strokes
    glEnable(GL_SCISSOR_TEST)
    GLWindow.setup_window(window, true)
    glDisable(GL_SCISSOR_TEST)
    glViewport(0,0, wh...)
    #Read all the selection queries
    GLWindow.push_selectionqueries!(window)
    Gtk.attach_buffers(gtk_area) # transfer back to window
    glClearColor(0,0,0,0)
    glClear(GL_COLOR_BUFFER_BIT)
    GLAbstraction.render(fb.postprocess[3]) # copy postprocess
    return
end
function connect_renderloop(gl_area, resolution, mesh_color)

end
function setup_screen()
    resolution = (600, 500)
    name = "GTK + GLVisualize"
    parent = Gtk.Window(name, resolution..., true, true)
    Gtk.visible(parent, true)
    Gtk.setproperty!(parent, Symbol("is-focus"), false)
    box = Gtk.Box(:v)
    push!(parent, box)
    mesh_color = Signal(RGBA{Float32}(1,0,0,1))
    gl_area = Gtk.GLArea()
    Gtk.gl_area_set_required_version(gl_area, 3, 3)
    GLAbstraction.new_context()
    push!(box, gl_area)
    Gtk.setproperty!(box, :expand, gl_area, true)
    Gtk.showall(parent)
    Gtk.signal_connect(gl_area, "render") do gl_area, gdk_context
        if !isassigned(screen)
            init_screen(gl_area, resolution, mesh_color)
        end
        render_gtk(screen[], gl_area)
        glFlush()
        return false
    end
    return parent
end

parent = setup_screen();
Gtk.showall(parent)

using Gtk, GLVisualize, GLWindow

function window_area!(window::Screen)
    callback = Gtk.@sigatom function (widget::Gtk.GtkGLArea, width::Int32, height::Int32)
        rect_signal = window.inputs[:window_area]
        push!(rect_signal, IRect(minimum(value(rect_signal)), width, height))
        return true
    end
    gl_area = window.glcontext.window # the Gtk.gl_area
    signal_connect(callback, gl_area, "resize")
    return
end

function scroll_event!(window::Screen)
    callback = Gtk.@sigatom function (widget::Gtk.GtkGLArea, s::Gtk.GdkEventScroll)
        if s.event_type == Gtk.GdkEventType.GDK_SCROLL
            scroll = window.inputs[:scroll]
            dirs = if s.direction == 0
                (0.0, 1.0)
            elseif s.direction == 1
                (0.0, -1.0)
            elseif s.direction == 2
                (-1.0, 0.0)
            elseif s.direction == 3
                (1.0, 0.0)
            else
                (0.0, 0.0)
            end
            push!(scroll, dirs)
            push!(scroll, (0.0, 0.0))
        end
        return true
    end
    gl_area = window.glcontext.window # the Gtk.gl_area
    signal_connect(callback, gl_area, "scroll-event")
    add_events(gl_area, GConstants.GdkEventMask.SCROLL) # same
    return
end

function to_mouse_button(x)
    if x == 1
        0
    elseif x == 2
        1
    elseif x == 3
        2
    else
        # TODO turn into error
        warn("Button is $x, while $(Gtk.GdkModifierType.BUTTON1)")
        Mouse.left
    end
end
function mousebuttons!(window::Screen)
    callback = Gtk.@sigatom function (widget::Gtk.GtkGLArea, event::Gtk.GdkEventButton)
        button = to_mouse_button(event.button)
        action = event.event_type
        if (action in Gtk.GdkEventType.BUTTON_PRESS:Gtk.GdkEventType.TRIPLE_BUTTON_PRESS) ||
                action == Gtk.GdkEventType.BUTTON_RELEASE

            set_s = window.inputs[:mouse_buttons_pressed]
            set = value(set_s)
            if action in Gtk.GdkEventType.BUTTON_PRESS:Gtk.GdkEventType.TRIPLE_BUTTON_PRESS
                push!(set, button)
            elseif action == GdkEventType.BUTTON_RELEASE
                delete!(set, button)
            else
                warn("unknown action: $(action)")
            end
            push!(set_s, set) # trigger event
        end
        return true
    end
    gl_area = window.glcontext.window
    Gtk.add_events(gl_area,
        Gtk.GConstants.GdkEventMask.GDK_BUTTON_PRESS_MASK |
        Gtk.GConstants.GdkEventMask.GDK_BUTTON_RELEASE_MASK
    )
    Gtk.signal_connect(callback, gl_area, "button_press_event")
    Gtk.signal_connect(callback, gl_area, "button_release_event")
    return
end
function mouse!(window::Screen)
    callback = Gtk.@sigatom function (widget::Gtk.GtkGLArea, s::Gtk.GdkEventMotion)
        if s.event_type == Gtk.GdkEventType.GDK_MOTION_NOTIFY
            pos = window.inputs[:mouseposition]
            push!(pos, (s.x, s.y))
        end
        return true
    end
    gl_area = window.glcontext.window # the Gtk.gl_area
    add_events(gl_area, GConstants.GdkEventMask.POINTER_MOTION)
    signal_connect(callback, gl_area, "motion-notify-event")
    return true
end
scroll_event!(screen[])
mouse!(screen[])
mousebuttons!(screen[])


module ObjMesh

include("graphics.jl")
include("utils.jl")

using RayTracer: Vec3, parse_mtllib!
using .Graphics

cache = Dict()

export ObjectMesh

mutable struct ObjectMesh
    ##
    #Load and render OBJ model files
    ##
    min_coords::Vector{Float32}
    max_coords::Vector{Float32}
    vlists::Vector{Array{Float32, 3}}
    texclists::Vector{Array{Float32, 3}}
    clists::Vector{Vector{Vec3}}
    textures::Vector{Union{Vec3, Nothing}}
end

function ObjectMesh(file_path::String)
    ##
    #Load an OBJ model file
    #
    #Limitations:
    #- only one object/group
    #- only triangle faces
    ##

    # Comments
    # mtllib file_name
    # o object_name
    # v x y z
    # vt u v
    # vn x y z
    # usemtl mtl_name
    # f v0/t0/n0 v1/t1/n1 v2/t2/n2

    #logger.debug('loading mesh "%s"' % os.path.basename(file_path))

    # Attempt to load the materials library
    materials = _load_mtl(file_path)
    mesh_file = open(file_path, "r")

    verts = Vector{Vector{Float32}}()
    texs = Vector{Vector{Float32}}()
    normals = Vector{Vector{Float32}}()
    faces = []

    cur_mtl = ""

    # For each line of the input file
    for line in eachline(mesh_file)
        line = rstrip(line, [' ', '\r', '\n'])

        # Skip comments
        (startswith(line, '#') || line == "") && continue

        tokens = split(line, " ")
        tokens = map(t -> strip(t, ' '), tokens)
        tokens = collect(filter(t -> t != "", tokens))

        prefix = tokens[1]
        tokens = tokens[2:end]

        if prefix == "v"
            vert = map(v->parse(Float32, v), tokens)
            push!(verts, vert)
        end

        if prefix == "vt"
            tc = map(v -> parse(Float32, v), tokens)
            push!(texs, tc)
        end

        if prefix == "vn"
            normal = map(v -> parse(Float32, v), tokens)
            push!(normals, normal)
        end

        if prefix == "usemtl"
            mtl_name = tokens[1]
            if mtl_name in keys(materials)
                cur_mtl = mtl_name
            else
                cur_mtl = ""
            end
        end

        if prefix == "f"
            @assert length(tokens) == 3 "only triangle faces are supported"

            face = Vector{Vector{Int}}()
            for token in tokens
                indices = filter(t -> t != "", split(token, '/'))
                indices = map(i -> parse(Int, i), indices)
                @assert length(indices) == 2 || length(indices) == 3
                push!(face, indices)
            end

            push!(faces, [face, cur_mtl])
        end
    end

    # Sort the faces by material name
    sort!(faces, by=f->f[2])
    # Compute the start and end faces for each chunk in the model
    cur_mtl = nothing
    chunks = []
    for (idx, face) in enumerate(faces)
        face, mtl_name = face
        if mtl_name != cur_mtl
            if length(chunks) > 0
                chunks[end]["end_idx"] = idx-1
            end
            push!(chunks, Dict([
                "mtl"=>materials[mtl_name],
                "start_idx"=>idx,
                "end_idx"=>nothing
            ]))
            cur_mtl = mtl_name
        end
    end

    chunks[end]["end_idx"] = length(faces)
    num_faces = length(faces)
    # logger.debug('num verts=%d' % len(verts))
    # logger.debug('num faces=%d' % num_faces)
    # logger.debug('num chunks=%d' % len(chunks))

    # Create arrays to store the vertex data
    list_verts = zeros(Float32, 3, 3, num_faces)
    list_norms = zeros(Float32, 3, 3, num_faces)
    list_texcs = zeros(Float32, 3, 2, num_faces)
    list_colors = Vector{Vec3}()

    # For each triangle
    for (f_idx, face) in enumerate(faces)
        face, mtl_name = face

        # Get the color for this face
        f_mtl = materials[mtl_name]
        #NOTE: May get some failure/error here because of else condition
        #f_color = !isempty(f_mtl) ? f_mtl.color_diffuse : rgb(1f0)
        f_color = f_mtl.color_diffuse
        # For each tuple of indices
        for (l_idx, indices) in enumerate(face)
            # Note: OBJ uses 1-based indexing
            # and texture coordinates are optional
            if length(indices) == 3
                v_idx, t_idx, n_idx = indices
                vert = verts[v_idx]
                texc = texs[t_idx]
                normal = normals[n_idx]
            else
                v_idx, n_idx = indices
                vert = verts[v_idx]
                normal = normals[n_idx]
                texc = [0f0, 0f0]
            end

            list_verts[l_idx, :, f_idx] .= vert
            list_texcs[l_idx, :, f_idx] .= texc
            list_norms[l_idx, :, f_idx] .= normal
            push!(list_colors, f_color)
        end
    end

    # Re-center the object so that the base is at y=0
    # and the object is centered in x and z
    min_coords = minimum(minimum(list_verts, dims=3), dims=1)
    max_coords = minimum(maximum(list_verts, dims=3), dims=1)

    mean_coords = (min_coords .+ max_coords) / 2f0
    #NOTE: Why is mean coords correct but min coords has error margin of ~0.02?
    #@show mean_coords
    min_y = min_coords[2]
    mean_x = mean_coords[1]
    mean_z = mean_coords[3]
    list_verts[:, 2, :] .= list_verts[:, 2, :] .- min_y
    list_verts[:, 1, :] .= list_verts[:, 1, :] .- mean_x
    list_verts[:, 3, :] .= list_verts[:, 3, :] .- mean_z

    # Recompute the object extents after centering
    min_coords = minimum(minimum(list_verts, dims=3), dims=1)[1, :, 1]
    max_coords = minimum(maximum(list_verts, dims=3), dims=1)[1, :, 1]

    # Vertex list, one per chunk
    vlists = Vector{Array{Float32, 3}}()

    # Texture Coordinates list corresponding to vlist
    texclists = Vector{Array{Float32, 3}}()

    # Color list
    clists = Vector{Vector{Vec3}}()

    # Textures, one per chunk
    textures = Vector{Union{Vec3, Nothing}}()

    # For each chunk
    for chunk in chunks
        start_idx = chunk["start_idx"]
        end_idx = chunk["end_idx"]
        num_faces_chunk = end_idx - start_idx + 1

        # Create a vertex list to be used for rendering

        #TODO
        #vlist = pyglet.graphics.vertex_list(
            #3num_faces_chunk,
            #("v3f", reshape(list_verts[:, :, start_idx:end_idx], :)),
            #("t2f", reshape(list_texcs[:, :, start_idx:end_idx], :)),
            #("n3f", reshape(list_norms[:, :, start_idx:end_idx], :)),
            #("c3f", reshape(list_color[:, :, start_idx:end_idx], :)))

        mtl = chunk["mtl"]

        texture = mtl.texture_diffuse

        push!(vlists, list_verts[:, :, start_idx:end_idx])
        push!(texclists, list_texcs[:, :, start_idx:end_idx])
        push!(clists, list_colors[start_idx:end_idx])
        push!(textures, texture)
    end

    ObjectMesh(min_coords, max_coords, vlists, texclists, clists, textures)
end

function get(mesh_name::String)
    ##
    #Load a mesh or used a cached version
    ##

    # Assemble the absolute path to the mesh file
    file_path = get_file_path("src/meshes", mesh_name, "obj")

    file_path ∈ keys(cache) && (return cache[file_path])

    mesh = ObjectMesh(file_path)
    cache[file_path] = mesh

    return mesh
end

function _load_mtl(model_file::String)
    texture_diffuse = nothing
    model_dir, file_name = splitdir(model_file) .* ""

    # Determine the default texture path for the default material
    tex_name = split(file_name, '.')[1] * ""
    tex_path = get_file_path("src/textures", tex_name, "png")

    if ispath(tex_path)
        texture_diffuse = load_texture(tex_path)
    end

    materials = Dict{String, Union{NamedTuple,Nothing}}([
        # This is default material
        ""=> (color_diffuse = Vec3(1.0f0),
              color_ambient = Vec3(1.0f0),
              color_specular = Vec3(1.0f0),
              specular_exponent = 50.0f0,
              texture_ambient = nothing,
              texture_diffuse = texture_diffuse,
              texture_specular = nothing)
        ])

    mtl_path = model_file[1:end-4] * ".mtl"

    if ispath(mtl_path)
        parse_mtllib!(mtl_path, materials, Float32)
    end

    return materials
end

function render(obj_mesh::ObjectMesh)
    for (idx, vlist) in enumerate(obj_mesh.vlists)
        texture = obj_mesh.textures[idx]

        if !isnothing(texture)
            #gl.glEnable(gl.GL_TEXTURE_2D)
            #gl.glBindTexture(texture.target, texture.id)
        else
            #gl.glDisable(gl.GL_TEXTURE_2D)
        end

        #vlist.draw(gl.GL_TRIANGLES)
    end

    #gl.glDisable(gl.GL_TEXTURE_2D)
end

end #module


# *********************************************************************************
# REopt, Copyright (c) 2019-2020, Alliance for Sustainable Energy, LLC.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# Redistributions of source code must retain the above copyright notice, this list
# of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright notice, this
# list of conditions and the following disclaimer in the documentation and/or other
# materials provided with the distribution.
#
# Neither the name of the copyright holder nor the names of its contributors may be
# used to endorse or promote products derived from this software without specific
# prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
# OF THE POSSIBILITY OF SUCH DAMAGE.
# *********************************************************************************

struct MPCInputs <: AbstractInputs
    s::MPCScenario
    techs::Techs
    existing_sizes::DenseAxisArray{Float64, 1}  # (techs.all)
    time_steps::UnitRange
    time_steps_with_grid::Array{Int, 1}
    time_steps_without_grid::Array{Int, 1}
    hours_per_time_step::Float64
    months::UnitRange
    production_factor::DenseAxisArray{Float64, 2}  # (techs.all, time_steps)
    levelization_factor::Dict{String, Float64}  # (techs.all)
    value_of_lost_load_per_kwh::Array{R, 1} where R<:Real #default set to 1 US dollar per kwh
    pwf_e::Float64
    pwf_om::Float64
    pwf_fuel::Dict{String, Float64}
    third_party_factor::Float64
    ratchets::UnitRange
    techs_by_exportbin::DenseAxisArray{Array{String,1}}  # indexed on [:NEM, :WHL]
    export_bins_by_tech::Dict{String, Array{Symbol, 1}}
    cop::Dict{String, Float64}  # (techs.cooling)
    thermal_cop::Dict{String, Float64}  # (techs.absorption_chiller)
end

function MPCInputs(fp::String)
    s = MPCScenario(JSON.parsefile(fp))
    MPCInputs(s)
end

function MPCInputs(s::MPCScenario)

    time_steps = 1:length(s.electric_load.loads_kw)
    hours_per_time_step = 1 / s.settings.time_steps_per_hour
    techs, production_factor, existing_sizes = setup_tech_inputs(s)
    months = 1:length(s.electric_tariff.monthly_demand_rates)

    techs_by_exportbin = DenseAxisArray([ techs.all, techs.all, techs.all], s.electric_tariff.export_bins)
    # TODO account for which techs have access to export bins (when we add more techs than PV)

    levelization_factor = Dict(t => 1.0 for t in techs.all)
    pwf_e = 1.0
    pwf_om = 1.0
    pwf_fuel = Dict{String, Float64}()
    pwf_fuel["Generator"] = 1.0
    third_party_factor = 1.0

    time_steps_with_grid, time_steps_without_grid, = setup_electric_utility_inputs(s)

    export_bins_by_tech = Dict{String, Array{Symbol, 1}}()
    for t in techs.elec
        export_bins_by_tech[t] = s.electric_tariff.export_bins
    end
    # TODO implement export bins by tech (rather than assuming that all techs share the export_bins)

    #Placeholder COP because the REopt model expects it
    cop = Dict("ExistingChiller" => s.cooling_load.cop)
    thermal_cop = Dict{String, Float64}()

    MPCInputs(
        s,
        techs,
        existing_sizes,
        time_steps,
        time_steps_with_grid,
        time_steps_without_grid,
        hours_per_time_step,
        months,
        production_factor,
        levelization_factor,  # TODO need this?
        typeof(s.financial.value_of_lost_load_per_kwh) <: Array{<:Real, 1} ? s.financial.value_of_lost_load_per_kwh : fill(s.financial.value_of_lost_load_per_kwh, length(time_steps)),
        pwf_e,
        pwf_om,
        pwf_fuel,
        third_party_factor,
        # maxsize_pv_locations,
        1:length(s.electric_tariff.tou_demand_ratchet_time_steps),  # ratchets
        techs_by_exportbin,
        export_bins_by_tech,
        cop,
        thermal_cop
        # s.site.min_resil_time_steps,
        # s.site.mg_tech_sizes_equal_grid_sizes,
        # s.site.node
    )
end

function setup_tech_inputs(s::MPCScenario)

    techs = Techs(s)

    time_steps = 1:length(s.electric_load.loads_kw)

    # REoptInputs indexed on techs:
    existing_sizes = DenseAxisArray{Float64}(undef, techs.all)
    production_factor = DenseAxisArray{Float64}(undef, techs.all, time_steps)

    if !isempty(techs.pv)
        setup_pv_inputs(s, existing_sizes, production_factor)
    end

    if "Generator" in techs.all
        setup_gen_inputs(s, existing_sizes, production_factor)
    end

    return techs, production_factor, existing_sizes
end

function setup_pv_inputs(s::MPCScenario, existing_sizes, production_factor)
    for pv in s.pvs
        production_factor[pv.name, :] = pv.prod_factor_series
        existing_sizes[pv.name] = pv.size_kw
    end
    return nothing
end

function setup_gen_inputs(s::MPCScenario, existing_sizes, production_factor)
    existing_sizes["Generator"] = s.generator.size_kw
    production_factor["Generator", :] = ones(length(s.electric_load.loads_kw))
    return nothing
end


#
# Load DFTK-compatible structural information from an external file
# Relies on ASE and other external libraries to do the parsing
#
using PyCall

function load_lattice(T, file::AbstractString)
    ase = pyimport_e("ase")
    ispynull(ase) && error("Install ASE to load data from exteral files")
    load_lattice(T, pyimport("ase.io").read(file))
end

function load_atoms(T, file::AbstractString)
    ase = pyimport_e("ase")
    ispynull(ase) && error("Install ASE to load data from exteral files")
    load_atoms(T, pyimport("ase.io").read(file))
end

function load_magnetic_moments(file::AbstractString)
    ase = pyimport_e("ase")
    ispynull(ase) && error("Install ASE to load data from exteral files")
    load_magnetic_moments(pyimport("ase.io").read(file))
end

"""
Return a DFTK-compatible `lattice` object loaded from an ASE `Atoms`, a pymatgen `Structure`
or a compatible file (e.g. xyz file, cif file etc.)
"""
load_lattice(args...; kwargs...) = load_lattice(Float64, args...; kwargs...)

"""
Return a DFTK-compatible `atoms` object loaded from an ASE `Atoms`, a pymatgen `Structure`
or a compatible file (e.g. xyz file, cif file etc.)
"""
load_atoms(  args...; kwargs...) = load_atoms(  Float64, args...; kwargs...)

load_model(  args...; kwargs...) = load_model(  Float64, args...; kwargs...)
load_basis(  args...; kwargs...) = load_basis(  Float64, args...; kwargs...)
load_density(args...; kwargs...) = load_density(Float64, args...; kwargs...)


import Soundcard
using Test
using Statistics
using SharedArrays
using Libaudio

let (ndev, digest) = Soundcard.device()
    @test ndev > 0
    @info "Soundcard device populate" digest
end

let x = 10^(-20/20) * randn(192000,1), ms = zeros(1,2)
    ms[1,2] = 1
    y = Soundcard.play(x, ms, 48000)
    @test y == nothing
end

let mm = zeros(8,1)
    mm[2,1] = 1
    y = Soundcard.record(48000, mm, 48000)
    @test typeof(y) == Matrix{Float32}
end

let x = 10^(-20/20) * randn(192000,1), ms = zeros(1,2), mm = zeros(8,1)
    ms[1,1] = 1
    mm[2,1] = 1
    y = Soundcard.playrecord(x, ms, mm, 48000)
    @info "loop gain" std(y)/std(x)
    @test std(y)/std(x) < 1e-4

    ms[1,2] = 1
    mm[2,1] = 1
    y = Soundcard.playrecord(x, ms, mm, 48000)
    @info "loop gain" std(y)/std(x)
    @test std(y)/std(x) > 0.1
end

let out = 10^(-20/20) * randn(192000,1), ms = zeros(1,2)
    ms[1,2] = 1
    x = Soundcard.mixer(convert(Matrix{Float32},out), ms)
    y = SharedArray{Float32,1}(Soundcard.interleave(x))
    r = Soundcard.play(size(x), y, 48000)
        # async tasks...
    # fetch(done)
    @info r
    @test r == 0
end

let out = 10^(-20/20) * randn(192000,1), mm = zeros(8,1)
    mm[2,1] = 1
    samples = 96000
    y = SharedArray{Float32,1}(zeros(Float32, size(mm,1)*samples))
    Soundcard.record(y, size(mm), samples, 48000)
        # async tasks...
    # fetch(done)
    recording = Soundcard.mixer(transpose(reshape(y,size(mm,1),samples)), mm)
    @test typeof(recording) == Matrix{Float32}
end

let out = 10^(-20/20) * randn(192000,1), ms = zeros(1,2), mm = zeros(8,1)
    ms[1,1] = 1
    mm[2,1] = 1
    samples = size(out,1)
    x = Soundcard.mixer(convert(Matrix{Float32},out), ms)
    y = SharedArray{Float32,1}(Soundcard.interleave(x))
    z = SharedArray{Float32,1}(zeros(Float32,size(mm,1)*samples))
    r = Soundcard.playrecord(size(x), y, z, size(mm), 48000)
        # async tasks...
    # fetch(done)
    recording = Soundcard.mixer(transpose(reshape(z,size(mm,1),samples)), mm)
    @info r
    @test r == 0
    @test typeof(recording) == Matrix{Float32}
end

let s = 10^(-10/20) * sinpi.(2*1000*(0:48000-1)/48000), ms = zeros(1,2), mm = zeros(8,1)
    x = s[:,:]
    ms[1,2] = 1
    mm[2,1] = 1
    y = Soundcard.playrecord(x, ms, mm, 48000)
    Libaudio.wavwrite("loop.wav", y, 48000, 32)
    # m = Libaudio.zerocrossingrate(s)
    # n = Libaudio.zerocrossingrate(y[:,1])
    # @info length(m[m.==1.0])
    # @info length(n[n.==1.0])
end


import Base.Iterators: Cycle

shader_symbol(::Type{<:ComponentData}) = :none

@component struct DioEntity end

#TODO get rid of this in favor of a correct iterator
function shared_entities(c::SharedComponent{T}, dat::T) where {T}
    ids = Int[]
    id = findfirst(x -> x == dat, c.shared)
    return findall(x -> x == id, data(c))
end

# DEFAULT COMPONENTS
abstract type Vao <: ComponentData end

macro vao(name)
    return esc(quote
                   @component @with_kw mutable struct $name <: Vao
                       vertexarray::VertexArray
                   end
               end)
end

macro instanced_vao(name)
    return esc(quote
                   @grouped_component @with_kw mutable struct $name <: Vao
                       vertexarray::VertexArray
                   end
               end)
end

Base.length(vao::Vao) = length(vao.vertexarray)
GLA.bind(vao::Vao) = GLA.bind(vao.vertexarray)

GLA.draw(vao::Vao) = GLA.draw(vao.vertexarray)

GLA.upload!(vao::Vao; kwargs...) = GLA.upload!(vao.vertexarray; kwargs...)

@component @with_kw struct Visible
    visible::Bool = true
end

# NON rendering Components
@component struct Dynamic end
@component @with_kw struct Spatial
    position::Point3f0 = zero(Point3f0)
    velocity::Vec3f0   = zero(Vec3f0)
end

function Base.:(+)(s1::Spatial, s2::Spatial)
    return Spatial(s1.position + s2.position, s1.velocity + s2.velocity)
end
function Base.:(-)(s1::Spatial, s2::Spatial)
    return Spatial(s1.position - s2.position, s1.velocity - s2.velocity)
end

@component struct Rotation
    q::Quaternions.Quaternion{Float32}
end
Rotation(axis::Vec, angle::Number) = Rotation(Quaternions.qrotation(axis, angle))
Rotation(axis1::Vec, axis2::Vec) = Rotation(rotation(axis1, axis2))

Base.:(*)(r1::Rotation, r2::Rotation) = Rotation(r1.q * r2.q)

Quaternions.angleaxis(r::Rotation) = Quaternions.angleaxis(r.q)
Quaternions.axis(r::Rotation)      = Quaternions.axis(r.q)
Quaternions.angle(r::Rotation)     = Quaternions.angle(r.q)

GeometryBasics.direction(r::Rotation) = r.q * Z_AXIS

@component @with_kw struct Shape
    scale::Vec3f0 = Vec3f0(1.0f0)
end
Shape(f::Real) = Shape(Vec3f0(f))
Base.length(::Type{Shape}) = 3
Base.eltype(::Type{Shape}) = Float32

@component @with_kw struct ModelMat
    modelmat::Mat4f0 = Eye4f0()
end
Base.length(::Type{ModelMat}) = 16
Base.eltype(::Type{ModelMat}) = Float32
shader_symbol(::Type{ModelMat}) = :modelmat
Base.size(::Type{ModelMat}) = size(Mat4f0)

@component @with_kw struct Material
    specpow::Float32 = 0.8f0
    specint::Float32 = 0.8f0
end
Base.eltype(::Type{Material}) = Float32
Base.length(::Type{Material}) = 2
shader_symbol(::Type{Material}) = :material

@component @with_kw struct PointLight
    diffuse::Float32  = 0.5f0
    specular::Float32 = 0.5f0
    ambient::Float32  = 0.5f0
end
Base.length(::Type{PointLight}) = 3
Base.eltype(::Type{PointLight}) = Float32

@component struct DirectionLight
    direction :: Vec3f0
    diffuse   :: Float32
    specular  :: Float32
    ambient   :: Float32
end

# Meshing and the like
@grouped_component struct Mesh
    mesh::Any
end

@component struct Alpha
    α::Float32
end
Base.length(::Type{Alpha}) = 1
Base.eltype(::Type{Alpha}) = Float32
shader_symbol(::Type{Alpha}) = :alpha

abstract type Color <: ComponentData end

# one color, will be put as a uniform in the shader
@component @with_kw struct UniformColor <: Color
    color::RGBf0 = DEFAULT_COLOR
end
UniformColor(x, y, z) = UniformColor(RGBf0(x, y, z))
Base.length(::Type{UniformColor}) = 3
Base.eltype(::Type{UniformColor}) = Float32
shader_symbol(::Type{UniformColor}) = :color

# vector of colors, either supplied manually or filled in by mesher
@component struct BufferColor <: Color
    color::Vector{RGBf0}
end

# color function, mesher uses it to throw in points and get out colors
#TODO super slow
@component struct FunctionColor <: Color
    color::Function
end

@component struct DensityColor <: Color
    color::Array{RGBf0,3}
end

# Cycle, mesher uses it to iterate over together with points
@component struct CycledColor <: Color
    color::Cycle{Union{RGBAf0,Vector{RGBAf0}}}
end

@shared_component struct Grid
    points::Array{Point3f0,3}
end

abstract type Geometry <: ComponentData end

@component struct PolygonGeometry <: Geometry #spheres and the like
    geometry::Any
end

@component struct FileGeometry <: Geometry #.obj files
    geometry::String
end

@component struct FunctionGeometry <: Geometry
    geometry :: Function
    iso      :: Float32
end

@component struct DensityGeometry <: Geometry
    geometry :: Array{Float32,3}
    iso      :: Float32
end

@component struct VectorGeometry <: Geometry
    geometry::Vector{Point3f0}
end

@component struct LineGeometry <: Geometry
    points::Vector{Point3f0}
    function LineGeometry(points::Vector{Point3f0})
        if length(points) < 4
            insert!(points, 1, points[2] + 1.001 * (points[1] - points[2]))
        end
        if length(points) < 4
            push!(points, points[end-1] + 1.001 * (points[end] - points[end-1]))
        end
        return new(points)
    end
end

@component @with_kw struct LineOptions
    thickness::Float32 = 2.0f0
    miter::Float32     = 0.6f0
end

@component @with_kw struct Text
    str::String = "test"
    font_size::Float64 = 20
    font = default_font()
    align::Tuple{Symbol,Symbol} = (:bottom, :right)
    offset::Vec3f0 = zero(Vec3f0)
end


# the main visualization module for OpenGL abstraction rendering
module vis

using GLFW
using ModernGL
using Serialization

struct Context
  window::GLFW.Window
  program::GLuint
  width::Int
  height::Int
  attributes::Array{GLint}
  Tmat_location::GLint
end

include("vis_util.jl")
include("vis_drawing.jl")
include("vis_scene.jl")

function make_shader_program(vsh::String, fsh::String)
  createcontextinfo()
  version_str = get_glsl_version_string()

  vertexShader = createShader(version_str * vsh, GL_VERTEX_SHADER)
  fragmentShader = createShader(version_str * fsh, GL_FRAGMENT_SHADER)
  program = createShaderProgram(vertexShader, fragmentShader)

  return program
end

function make_context(width, height)
  # Create a window and its OpenGL context
  GLFW.WindowHint(GLFW.SAMPLES, 4)
  GLFW.WindowHint(GLFW.CONTEXT_VERSION_MAJOR, 3)
  GLFW.WindowHint(GLFW.CONTEXT_VERSION_MINOR, 3)
  GLFW.WindowHint(GLFW.OPENGL_FORWARD_COMPAT, Cint(1)) # true, required by OS X
  GLFW.WindowHint(GLFW.OPENGL_PROFILE, GLFW.OPENGL_CORE_PROFILE)
  window = GLFW.CreateWindow(width, height, "window.jl")

  # Make the window's context current
  GLFW.MakeContextCurrent(window)

  # Compile the Shader Program
  dir_path = @__DIR__
  vsh = read(dir_path * "/shaders/shader.vert", String)
  fsh = read(dir_path * "/shaders/shader.frag", String)
  program = make_shader_program(vsh, fsh)
  glUseProgram(program)

  positionAttribute = glGetAttribLocation(program, "position")
  colorAttribute = glGetAttribLocation(program, "color")
  usetexAttribute = glGetAttribLocation(program, "usetex")
  texcoordAttribute = glGetAttribLocation(program, "texcoord")
  attributes = [positionAttribute, colorAttribute, usetexAttribute,
                texcoordAttribute]
  Tmat_location = glGetUniformLocation(program, "Tmat")

  return Context(window, program, width, height, attributes, Tmat_location)
end

function print_error_if_not_empty()
  err = glErrorMessage()
  if err != ""
    println(err)
  end
end

function setup()
  # Load the Window -----------------------------------------------------------
  width = 1080
  height = round(Int, 1080 * 9 / 16)
  context = make_context(width, height)
  # ---------------------------------------------------------------------------

  # window scaling matrix -----------------------------------------------------
  W = scale_mat(context.height/context.width, 1)
  glUniformMatrix4fv(glGetUniformLocation(context.program, "Wmat"), 1,
                     GL_FALSE, W)
  # ---------------------------------------------------------------------------

  # Load Font Texture ---------------------------------------------------------
  textureBuffer = glGenTexture()
  glActiveTexture(GL_TEXTURE0)

  dir_path = @__DIR__
  fp = open(dir_path * "/font/font.bin", "r")
  pixels = deserialize(fp)
  close(fp)
  (w, h) = size(pixels)
  w = div(w, 4)

  glEnable(GL_BLEND)
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
  glBindTexture(GL_TEXTURE_2D, textureBuffer)
  glBindTexture(GL_TEXTURE_2D, textureBuffer)
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_FLOAT, pixels);

  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

  glUniform1i(glGetUniformLocation(context.program, "tex"), 0)
  # ---------------------------------------------------------------------------

  return context
end

function test()
  context = setup()

  # Make a Bunch of Test Objects ----------------------------------------------
  # triangle
  position_data = GLfloat[-1.0, 1.0,
                          -1.0, -1.0,
                          -0.1, 0.0]
  position = RenderData(position_data, 2, GL_STATIC_DRAW)

  color_data = GLfloat[0.0, 0.0, 1.0,
                       1.0, 0.0, 0.0,
                       0.0, 1.0, 0.0]
  color = RenderData(color_data, 3, GL_STATIC_DRAW)
  usetex = RenderData(fill(GLfloat(0), 3), 1, GL_STATIC_DRAW)
  texcoord = RenderData(fill(GLfloat(0), 6), 2, GL_STATIC_DRAW)
  idx = GLuint[0, 1, 2]
  object2 = RenderObject(context, [position, color, usetex, texcoord], idx)

  # road
  th = range(0, stop=(2 * pi), length=30)
  r = 0.6
  x = fill(0.0, length(th))
  y = fill(0.0, length(th))
  for i in 1:length(th)
    x[i] = r * cos(th[i])
    y[i] = r * sin(th[i])
  end
  object3 = make_road(context, x, y, 0.1)

  # font square
  position_data = GLfloat[-1.0, 0.5,
                          -1.0, 1.0,
                          0.0, 1.0,
                          0.0, 0.5]
  position = RenderData(position_data, 2, GL_STATIC_DRAW)
  color = RenderData(repeat(GLfloat[1.0, 0.0, 0.0], 4), 3, GL_STATIC_DRAW)
  usetex = RenderData(fill(GLfloat(1.0), 4), 1, GL_STATIC_DRAW)
  texcoord_data = GLfloat[0.0, 1.0,
                          0.0, 0.0,
                          1.0, 0.0,
                          1.0, 1.0]
  texcoord = RenderData(texcoord_data, 2, GL_STATIC_DRAW)
  idx = GLuint[0, 1, 2,
               0, 2, 3]
  object4 = RenderObject(context, [position, color, usetex, texcoord], idx)

  # line
  color = [0.0, 0.0, 0.0]
  l1 = make_line(context, 0.0, 0.0, 0.5, -0.5, color)
  s1 = make_text(context, string(time_ns()), 0.0, 0.0)

  # vector
  vec1 = make_vector_xy(context, 0.2, -0.2, 0.5, 0.5)

  # car
  car1 = make_car(context)
  car1.T = scale_mat(0.01, 0.01)

  # Main Render Loop ----------------------------------------------------------
  k = 0
  text = ""

  println("Set run to false")
  t0 = time_ns() / 1e9
  window_status = true

  k = 0
  t = 0
  while window_status
    #println(1e3 * (time_ns() / 1e9 - t))
    t = time_ns() / 1e9
    points = GLfloat[0.0, 0.0, cos(t), sin(t)]
    update_buffer!(l1, points, context.attributes[1])
    if k == 60
      #text = string(rand(1:100))
      #update_text!(s1, text, 0.0, 0.0)
      car_lights!(car1)
      k = 0
    end
    update_text!(s1, string(time_ns()), 0.0, 0.0)
    car1.T = scale_mat(0.01, 0.01) * translate_mat(0, (t - t0) * 0.1)

    k += 1

    window_status = visualize(context, [object2, object3, object4, l1,
                                        s1, car1, vec1])
  end

  if window_status == true
    GLFW.DestroyWindow(context.window)
  end

  return
end

function visualize(context::Context, objects::Array{Any, 1})
  if GLFW.WindowShouldClose(context.window)
    GLFW.DestroyWindow(context.window)

    return false
  end

  glClearColor(0.3, 0.7, 0.0, 1.0)
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

  for obj in objects
    render(obj)
  end

  GLFW.SwapBuffers(context.window)
  GLFW.PollEvents()

  return true
end

#test()

end


# fit.jl

fit_nlopt!(lmm::MetidaModel; kwargs...)  = error("MetidaNLopt not found. \n - Run `using MetidaNLopt` before.")

"""
    fit!(lmm::LMM{T}; kwargs...
    ) where T

Fit LMM model.

# Keywords:

* `solver` - :default / :nlopt for using with MetidaNLopt.jl/ :cuda for using with MetidaCu.jl
* `verbose` - :auto / 1 / 2 / 3
* `varlinkf` - :exp / :sq / :identity [ref](./details/#Variance-parameters-link-function)
* `rholinkf` - :sigm / :atan / :sqsigm / :psigm
* `aifirst` - first iteration with AI-like method - :default / :ai / :score
* `g_tol` - absolute tolerance in the gradient
* `x_tol` - absolute tolerance of theta vector
* `f_tol` - absolute tolerance in changes of the REML
* `hes` - calculate REML Hessian
* `init` - initial theta values
* `refitinit` - true/false - if true - use last values for initial condition
* `optmethod` - Optimization method. Look at Optim.jl documentation. (Newton by default)
* `io` - output IO
"""
function fit!(lmm::LMM{T}; kwargs...) where T

    kwkeys = keys(kwargs)

    :solver ∈ kwkeys ? solver = kwargs[:solver] : solver = :default
    :verbose ∈ kwkeys ? verbose = kwargs[:verbose] : verbose = :auto
    :varlinkf ∈ kwkeys ? varlinkf = kwargs[:varlinkf] : varlinkf = :exp
    :rholinkf ∈ kwkeys ? rholinkf = kwargs[:rholinkf] : rholinkf = :sigm
    :aifirst ∈ kwkeys ? aifirst = kwargs[:aifirst] : aifirst = :default
    :g_tol ∈ kwkeys ? g_tol = kwargs[:g_tol] : g_tol = 1e-10
    :x_tol ∈ kwkeys ? x_tol = kwargs[:x_tol] : x_tol = :1e-10
    :f_tol ∈ kwkeys ? f_tol = kwargs[:f_tol] : f_tol = :1e-10
    :hes ∈ kwkeys ? hes = kwargs[:hes] : hes = true
    :init ∈ kwkeys ? init = kwargs[:init] : init = :nothing
    :io ∈ kwkeys ? io = kwargs[:io] : io = stdout
    :time_limit ∈ kwkeys ? time_limit = kwargs[:time_limit] : time_limit = 120
    :iterations ∈ kwkeys ? iterations = kwargs[:iterations] : iterations = 300
    :refitinit ∈ kwkeys ? refitinit = kwargs[:refitinit] : refitinit = false
    :optmethod ∈ kwkeys ? optmethod = kwargs[:optmethod] : optmethod = :default

    # If model was fitted, previous results can be used if `refitinit` == true
    # Before fitting clear log
    if lmm.result.fit
        if length(lmm.log) > 0 deleteat!(lmm.log, 1:length(lmm.log)) end
        lmmlog!(io, lmm, verbose, LMMLogMsg(:INFO, "Refit model..."))
        if refitinit
            lmmlog!(io, lmm, verbose, LMMLogMsg(:INFO, "Using previous initial parameters."))
            init = lmm.result.theta
        end
    end
    lmm.result.fit = false
    # Use default solver (Optim.jl with Newton)
    solver == :default || return fit_nlopt!(lmm; kwargs...)
    # Set verbose mode
    if verbose == :auto
        verbose = 1
    end
    # Optimization method
    if optmethod == :default
        optmethod  = NEWTON_OM
    end

    # Optimization function
    optfunc = reml_sweep_β
    # Make data views
    data = LMMDataViews(lmm)
    #Optim options
    optoptions = Optim.Options(g_tol = g_tol, x_tol = x_tol, f_abstol = f_tol,
        iterations = iterations,
        time_limit = time_limit,
        store_trace = true,
        show_trace = false,
        allow_f_increases = true,
        extended_trace = true,
        callback = optim_callback)
    ############################################################################
    # Initial variance
    θ  = zeros(T, lmm.covstr.tl)
    if isa(init, Vector{T})
        if length(θ) == length(init)
            copyto!(θ, init)
            lmmlog!(io, lmm, verbose, LMMLogMsg(:INFO, "Using provided θ: "*string(θ)))
        else
            error("init length $(length(init)) != θ length $(length(θ))")
        end
    else
        initθ = sqrt(initvar(lmm.data.yv, lmm.mm.m)[1])/(length(lmm.covstr.random)+1)
        #θ                      .= initθ
        for i = 1:length(θ)
            if lmm.covstr.ct[i] == :var
                θ[i] = initθ
            elseif lmm.covstr.ct[i] == :rho
                θ[i] = 1e-4
            end
        end
        lmmlog!(io, lmm, verbose, LMMLogMsg(:INFO, "Initial θ: "*string(θ)))
    end
    # Initial step with modified Newton method
    chunk  = ForwardDiff.Chunk{1}()
    if isa(aifirst, Bool)
        if aifirst aifirst == :ai else aifirst == :default end
    end
    ############################################################################
    if aifirst == :ai || aifirst == :score
        optstep!(lmm, data, θ; method = aifirst, maxopt = 10)
        lmmlog!(io, lmm, verbose, LMMLogMsg(:INFO, "First step with AI-like method ($aifirst), θ: "*string(θ)))
    end
    varlinkrvecapply!(θ, lmm.covstr.ct; varlinkf = varlinkf, rholinkf = rholinkf)

    # Twice differentiable object
    vloptf(x) = optfunc(lmm, data, varlinkvecapply(x, lmm.covstr.ct; varlinkf = varlinkf, rholinkf = rholinkf))[1]
    gcfg   = ForwardDiff.GradientConfig(vloptf, θ, chunk)
    hcfg   = ForwardDiff.HessianConfig(vloptf, θ, chunk)
    gfunc!(g, x) = ForwardDiff.gradient!(g, vloptf, x, gcfg, Val{false}())
    hfunc!(h, x) = begin
        ForwardDiff.hessian!(h, vloptf, x, hcfg, Val{false}())
    end
    td = TwiceDifferentiable(vloptf, gfunc!, hfunc!, θ)
    # Optimization object
    try
        lmm.result.optim  = Optim.optimize(td, θ, optmethod, optoptions)
    catch e
        lmmlog!(lmm, LMMLogMsg(:ERROR, "Newton method failed, try LBFGS. Error: $e"))
        optmethod  = LBFGS_OM
        lmm.result.optim  = Optim.optimize(td, θ, optmethod, optoptions)
    end
        # Theta (θ) vector
    lmm.result.theta  = varlinkvecapply!(deepcopy(Optim.minimizer(lmm.result.optim)), lmm.covstr.ct; varlinkf = varlinkf, rholinkf = rholinkf)
    lmmlog!(io, lmm, verbose, LMMLogMsg(:INFO, "Resulting θ: "*string(lmm.result.theta)*"; $(Optim.iterations(lmm.result.optim)) iterations."))

        # -2 LogREML, β, iC
    lmm.result.reml, lmm.result.beta, iC, θ₃, noerrors = optfunc(lmm, data, lmm.result.theta)
        # If errors in last evaluetion - log it.
    if !noerrors LMMLogMsg(:ERROR, "The last optimization step wasn't accurate. Results can be wrong!") end
        # Fit true
    if !isnan(lmm.result.reml) && !isinf(lmm.result.reml) && noerrors
        # Variance-vovariance matrix of β
        lmm.result.c            = inv(Matrix(iC))
        # SE
        if  !any(x-> x < 0.0, diag(lmm.result.c))
            lmm.result.se           = sqrt.(diag(lmm.result.c))
            if any(x-> x < 1e-8, lmm.result.se) && minimum(lmm.result.se)/maximum(lmm.result.se) < 1e-8 lmmlog!(io, lmm, verbose, LMMLogMsg(:WARN, "Some of the SE parameters is suspicious.")) end
            lmmlog!(io, lmm, verbose, LMMLogMsg(:INFO, "Model fitted."))
            lmm.result.fit      = true
        end
    end
    # Check G
    if lmm.covstr.random[1].covtype.s != :ZERO
        for i = 1:length(lmm.covstr.random)
            dg = det(gmatrix(lmm, i))
            if dg < 1e-8 lmmlog!(io, lmm, verbose, LMMLogMsg(:WARN, "det(G) of random effect $(i) is less 1e-08.")) end
        end
    end
    # Check Hessian
    if hes && lmm.result.fit
            # Hessian
        lmm.result.h      = hessian(lmm, lmm.result.theta)
            # H positive definite check
        if !isposdef(Symmetric(lmm.result.h))
            lmmlog!(io, lmm, verbose, LMMLogMsg(:WARN, "Hessian is not positive definite."))
        end
        qrd = qr(lmm.result.h, Val(true))
        for i = 1:length(lmm.result.theta)
            if abs(qrd.R[i,i]) < 1E-8
                if lmm.covstr.ct[qrd.jpvt[i]] == :var
                    lmmlog!(io, lmm, verbose, LMMLogMsg(:WARN, "Hessian parameter (variation) QR.R diagonal value ($(qrd.jpvt[i])) is less than 1e-8."))
                elseif lmm.covstr.ct[qrd.jpvt[i]] == :rho
                    lmmlog!(io, lmm, verbose, LMMLogMsg(:WARN, "Hessian parameter (rho) QR.R diagonal value ($(qrd.jpvt[i])) is less than 1e-8."))
                end
            end
        end
    end
    #
    if !lmm.result.fit
        lmmlog!(io, lmm, verbose, LMMLogMsg(:INFO, "Model NOT fitted."))
    end
    # return
    lmm
end

function optstep!(lmm, data, θ; method::Symbol = :ai, maxopt::Int=10)
    if method == :ai ai_func = sweep_ai else ai_func = sweep_score end
    reml, beta, θs₂, θ₃, rt = reml_sweep_β(lmm, data, θ)
    rt || error("Wrong initial conditions.")
    chunk  = ForwardDiff.Chunk{1}()
    aif(x) = ai_func(lmm, data, x, beta)
    grf(x) = reml_sweep_β(lmm, data, x, beta)[1]
    aihcfg = ForwardDiff.HessianConfig(aif, θ, chunk)
    aigcfg = ForwardDiff.GradientConfig(grf, θ, chunk)
    ai = ForwardDiff.hessian(aif, θ, aihcfg, Val{false}())
    gr = ForwardDiff.gradient(grf, θ, aigcfg, Val{false}())
    θt = similar(θ)
    try
        mul!(θt, inv(ai), gr)
    catch
        mul!(θt, pinv(ai), gr)
    end
    for i = 1:length(θ)
        if lmm.covstr.ct[i] == :rho
            if θ[i] - θt[i] > 1.0 - eps() θt[i] = θ[i] - 1.0 + eps() elseif θ[i] - θt[i] < -1.0 + eps() θt[i] = θ[i] + 1.0 - eps() end
        else
            if θ[i] - θt[i] < eps() θt[i] = θ[i] / 2.0 end
        end
    end
    local maxopti = maxopt
    local remlc
    while maxopti > 0
        θr = θ - θt
        remlc, beta, θs₂, θ₃, rt = reml_sweep_β(lmm, data, θr)
        maxopti -= 1
        if rt && remlc < reml return copyto!(θ, θr), remlc, maxopt-maxopti, true else θt ./= 2.0 end
    end
    return θ, remlc, maxopt-maxopti, false
end


# METHOD TEMPLATES
# basic method template: "This method has priority over the more general T:<Union{....} template"
# set_wfm(obj::INSTR{:Agilent33500B},func="sin",ch="1") = write(obj,"SOURce$ch:FUNC $func")
# more general method template, superseded by basic method template
# set_wfm(obj::T;func="sin",ch="1") where {T<:Union{F332x1,F335x2}} = write(obj,"SOURce$ch:FUNC $func")

## TRIGGER
"""
# Trigger source
`set_trig_sour(obj::T;ch="1",sour="BUS") where {T <: Union{DG1000Z}}`
- sour = {"INTernal", "EXTernal", "BUS"}
"""
set_trig_sour(obj::T;ch="1",sour="BUS") where {T <: Union{DG1000Z}} = write(obj, "TRIG$ch:SOUR $sour")

"""
# Trigger delay (del in seconds)
`set_trig_del(obj::T;ch="1",del=0) where {T <: Union{DG1000Z}}`
- del = delay in s
"""
set_trig_del(obj::T;ch="1",del=0) where {T <: Union{DG1000Z}} = write(obj, "TRIG$ch:DEL $del")

"""
# Trigger slope
`set_trig_slope(obj::T;ch="1",slp="positive") where {T <: Union{DG1000Z}}`
- slp = {"POSitive", "NEGative"}
"""
set_trig_slope(obj::T;ch="1",slp="positive") where {T <: Union{DG1000Z}} = write(obj, "TRIG$ch:SLOP $slp")

"""
# Output trigger state
"""
#set_out_trig_stat(obj::T;st="normal") where {T <: Union{DG1000Z}} = write(obj, "OUTP:TRIG $st")

"""
# Output trigger slope
"""
#set_out_trig_slope(obj::T;slp="positive") where {T <: Union{DG1000Z}} = write(obj, "OUTP:TRIG:SLOP $slp") # RG1022 has SWEep:TRIGger:TRIGOut and BURSt:TRIGger:TRIGOut

"""
# Software trigger
`send_soft_trig(obj::T) where {T <: Union{DG1000Z}}`
"""
send_soft_trig(obj::T) where {T <: Union{DG1000Z}} = write(obj, "*TRG")

## OUTPUT FUNCTION
"""
# Waveform
`set_wfm(obj::T;ch="1",func="sin") where {T <: Union{DG1000Z}}``
- func = {
    "SINusoid", "SQUare", "RAMP", "PULSe", "NOISe", "USER", "HARMonic", "CUSTom", "DC", "KAISER", " ROUNDPM", "SINC", "NEGRAMP", "ATTALT", "AMPALT", "STAIRDN", "STAIRUP", "STAIRUD", "CPULSE", " PPULSE", "NPULSE", "TRAPEZIA", "ROUNDHALF", "ABSSINE", "ABSSINEHALF", "SINETRA", " SINEVER", "EXPRISE", "EXPFALL", "TAN", "COT", "SQRT", "X2DATA", "GAUSS", "HAVERSINE", "LORENTZ ", "DIRICHLET", "GAUSSPULSE", "AIRY", "CARDIAC", "QUAKE", "GAMMA", "VOICE", "TV", "COMBIN", " BANDLIMITED", "STEPRESP", "BUTTERWORTH", "CHEBYSHEV1", "CHEBYSHEV2", "BOXCAR", " BARLETT", "TRIANG", "BLACKMAN", "HAMMING", "HANNING", "DUALTONE", "ACOS", "ACOSH", " ACOTCON", "ACOTPRO", "ACOTHCON", "ACOTHPRO", "ACSCCON", "ACSCPRO", "ACSCHCON", " ACSCHPRO", "ASECCON", "ASECPRO", "ASECH", "ASIN", "ASINH", "ATAN", "ATANH", "BESSELJ", " BESSELY", "CAUCHY", "COSH", "COSINT", "COTHCON", "COTHPRO", "CSCCON", "CSCPRO", " CSCHCON", "CSCHPRO", "CUBIC", "ERF", "ERFC", "ERFCINV", "ERFINV", "LAGUERRE", "LAPLACE", " LEGEND", "LOG", "LOGNORMAL", "MAXWELL", "RAYLEIGH", "RECIPCON", "RECIPPRO", "SECCON", " SECPRO", "SECH", "SINH", "SININT", "TANH", "VERSIERA", "WEIBULL", "BARTHANN", "BLACKMANH", " BOHMANWIN", "CHEBWIN", "FLATTOPWIN", "NUTTALLWIN", "PARZENWIN", "TAYLORWIN", " TUKEYWIN", "CWPUSLE", "LFPULSE", "LFMPULSE", "EOG", "EEG", "EMG", "PULSILOGRAM", "TENS1", " TENS2", "TENS3", "SURGE", "DAMPEDOSC", "SWINGOSC", "RADAR", "THREEAM", "THREEFM", " THREEPM", "THREEPWM", "THREEPFM", "RESSPEED", "MCNOSIE", "PAHCUR", "RIPPLE", " ISO76372TP1", "ISO76372TP2A", "ISO76372TP2B", "ISO76372TP3A", "ISO76372TP3B", " ISO76372TP4", "ISO76372TP5A", "ISO76372TP5B", "ISO167502SP", "ISO167502VR", "SRC", " IGNITION", "NIMHDISCHARGE", "GATEVIBR"
}
"""
set_wfm(obj::T;ch="1",func="sin") where {T <: Union{DG1000Z}} = write(obj, "SOURce$ch:FUNC $func")

"""
# Enable Arb Waveform output
`set_wfm_arb(obj::T;ch="1") where {T <: Union{DG1000Z}}``
"""
set_wfm_arb(obj::T;ch="1") where {T <: Union{DG1000Z}} = write(obj, "SOURce$ch:APPLy:ARBitrary")

"""
# Set Amplitude
`set_amplit(obj::T;ch="1",unit="vpp",volt=0.01) where {T <: Union{DG1000Z}}``
- unit = {"VPP", "VRMS", "DBM"}
- volt = voltage in <unit>
"""
set_amplit(obj::T;ch="1",unit="vpp",volt=0.01) where {T <: Union{DG1000Z}} = write(obj, "SOURce$ch:VOLT:UNIT $unit ; :SOURce$ch:VOLT $volt")

"""
# Set Offset
`set_offs(obj::T;ch="1",volt=0.01) where {T <: Union{DG1000Z}}`
- volt = offset voltage in Volts
"""
set_offs(obj::T;ch="1",volt=0.01) where {T <: Union{DG1000Z}} = write(obj, "SOURce$ch:VOLT:OFFset $volt")

"""
# Set High Level
`set_hilev(obj::T;ch="1",volt=0.01) where {T <: Union{DG1000Z}}`
- volt = High Level in Volts (= Offset + Amplitude/2)
"""
set_hilev(obj::T;ch="1",volt=0.01) where {T <: Union{DG1000Z}} = write(obj, "SOURce$ch:VOLT:HIGH $volt")
"""
# Set Low Level
`set_lolev(obj::T;ch="1",volt=0.01) where {T <: Union{DG1000Z}}`
- volt = Low Level in Volts (= Offset - Amplitude/2)
"""
set_lolev(obj::T;ch="1",volt=0.01) where {T <: Union{DG1000Z}} = write(obj, "SOURce$ch:VOLT:LOW $volt")

"""
# Set Frequency
`set_freq(obj::T;ch="1",fr=1000) where {T <: Union{DG1000Z}}`
- fr = Frequency in Hertz
"""
set_freq(obj::T;ch="1",fr=1000) where {T <: Union{DG1000Z}} = write(obj, "SOURce$ch:FREQ $fr")

"""
# Set Period
`set_per(obj::T;ch="1",per=0.001) where {T <: Union{DG1000Z}}`
- per = Period in Seconds
"""
set_per(obj::T;ch="1",per=0.001) where {T <: Union{DG1000Z}} = write(obj, "SOURce$ch:PER $per")

"""
# Set Phase
`set_phase(obj::T;ch="1",phase=90) where {T <: Union{DG1000Z}}`
- phase = Phase in Deg
"""
set_phase(obj::T;ch="1",phase=90) where {T <: Union{DG1000Z}} = write(obj, "SOURce$ch:PHAS $phase")

"""
# Set Symmetry
`set_sym(obj::T;ch="1",func="ramp",sym=50) where {T <: Union{DG1000Z}}`
- func = always RAMP for DG1000Z
- sym = 0% to 100%
"""
set_sym(obj::T;ch="1",func="ramp",sym=50) where {T <: Union{DG1000Z}} = write(obj, "SOURce$ch:FUNC:RAMP:SYMM $sym")

# Both Edge times
set_both_edg(obj::T;ch="1",func="pulse",dt=10e-9) where {T <: Union{DG1000Z}} = write(obj, "SOURce$ch:FUNC:$func:TRAN $dt")
# Lead Edge time
set_lead_edg(obj::T;ch="1",func="pulse",dt=10e-9) where {T <: Union{DG1000Z}} = write(obj, "SOURce$ch:FUNC:$func:TRAN:LEAD $dt")
# Trail Edge time
set_trail_edg(obj::T;ch="1",func="pulse",dt=10e-9) where {T <: Union{DG1000Z}} = write(obj, "SOURce$ch:FUNC:$func:TRAN:TRA $dt")
# Width
set_width(obj::T;ch="1",func="pulse",wd=1e-4) where {T <: Union{DG1000Z}} = write(obj, "SOURce$ch:FUNC:$func:WIDTh $wd")
#
## OUTPUT
# Output Load
set_load(obj::T;ch="1",load="INF") where {T <: Union{DG1000Z}} = write(obj, "OUTP$ch:LOAD $load")
# Output Polarity
set_pol(obj::T;ch="1",pol="normal") where {T <: Union{DG1000Z}} = write(obj, "OUTP$ch:POL $pol")

"""
# Set Output on/off
`set_outp(obj::T;ch="1",st="off") where {T <: Union{DG1000Z}}`
- st = {"on", "off"}
"""
set_outp(obj::T;ch="1",st="off") where {T <: Union{DG1000Z}} = write(obj, "OUTP$ch $st")

## SYNC
# Sync state
set_sync_stat(obj::T;ch="1",st="on") where {T <: Union{DG1000Z}} = write(obj, "OUTP$ch:SYNC $st")

## ARBITRARY WAVEFORMS
"""
# Set Arb mode
`set_arb_mode(obj::T;ch="1",mode="FREQ") where {T <: Union{DG1000Z}}``
- mode = {"FREQ", "SRATe"}
"""
set_arb_mode(obj::T;ch="1",mode="FREQ") where {T <: Union{DG1000Z}} = write(obj, "SOURce$ch:FUNC:ARB:MODE $mode")

"""
# Set sample rate for Arb mode
`set_sample_rate(obj::T;ch="1",srate=1e6) where {T <: Union{DG1000Z}}`
- srate = Sample rate in samples per second
"""
set_sample_rate(obj::T;ch="1",srate=1e6) where {T <: Union{DG1000Z}} = write(obj, "SOURce$ch:FUNC:ARB:SRAT $srate")

# Clear all arbs from volatile memory
# clear_arbs(obj::T;ch="1") where {T<:Union{DG1000Z}} = write(obj,"SOURce$ch:DATA:VOLATILE:CLEar") # Doesnt seem supported on the Rigol_DG1000Z

"""
# Load Arb data points to volatile memory
`set_arb_data(obj::T;ch="1",values="-1,-1,-0.5,-0.5,0,0.5,0.5,1.0,1.0") where {T <: Union{DG1000Z}}`
- values = -1.0 to +1.0
"""
set_arb_data(obj::T;ch="1",values="-1,-1,-0.5,-0.5,0,0.5,0.5,1.0,1.0") where {T <: Union{DG1000Z}} = write(obj, "SOURce$ch:DATA VOLATILE,$values")

"""
# Set number of data points for Arb mode
`set_arb_points(obj::T;ch="1",points=10) where {T <: Union{DG1000Z}}`
- points = 8 to 16384
"""
set_arb_points(obj::T;ch="1",points=10) where {T <: Union{DG1000Z}} = write(obj, "SOURce$ch:DATA:POIN VOLATILE,$points")

## Sweep

set_sweep_mode(obj::T; ch="1", st="off") where {T <: Union{DG1000Z}} = write(obj, "SOURce$ch:SWE:STAT $st")

"""
- time = 1ms to 500s (in seconds)
"""
set_sweep_time(obj::T; ch="1", time=1) where {T <: Union{DG1000Z}} = write(obj, "SOURce$ch:SWE:TIME $time")

set_sweep_start(obj::T; ch="1", freq::Integer=1000) where {T <: Union{DG1000Z}} = write(obj, ":SOUR$ch:FREQ:STAR $freq")

set_sweep_stop(obj::T; ch="1", freq::Integer=100) where {T <: Union{DG1000Z}} = write(obj, ":SOUR$ch:FREQ:STOP $freq")

set_sweep_center(obj::T; ch="1", freq::Integer=100) where {T <: Union{DG1000Z}} = write(obj, ":SOUR$ch:FREQ:CENT $freq")

set_sweep_span(obj::T; ch="1", span::Integer=100) where {T <: Union{DG1000Z}} = write(obj, ":SOUR$ch:FREQ:SPAN $span")