#include "macros.h"

module property_vector_pchb_doubles_select_particles
    use constants, only: dp, int64, stdout, n_int, bits_n_int
    use fortran_strings, only: to_upper
    use bit_rep_data, only: nIfD
    use util_mod, only: EnumBase_t, ByteSize_t
    use aliasSampling, only: AliasSampler_1D_t, AliasSampler_2D_t
    use SystemData, only: nEl, AB_elec_pairs, par_elec_pairs, t_mol_3_body, nBasis
    use dSFMT_interface, only: genrand_real2_dSFMT
    use FciMCData, only: pParallel, projEDet
    use sets_mod, only: operator(.in.)
    use excit_gens_int_weighted, only: pick_biased_elecs, get_pgen_pick_biased_elecs
    use util_mod, only: stop_all, operator(.isclose.), swap, &
        binary_search_int, EnumBase_t, operator(.div.)
    use MPI_wrapper, only: iProcIndex_intra
    use excitation_types, only: Excite_2_t
    use sltcnd_mod, only: nI_invariant_sltcnd_excit, sltcnd_excit
    use property_vector_index, only: PropertyIndexer_t, lookup_property_indexer
    better_implicit_none
    private
    public :: ParticleSelector_t, PC_FullyWeightedParticles_t, &
        UniformParticles_t, PC_FastWeightedParticles_t, &
        PCHB_ParticleSelection_t, allocate_and_init, &
        PCHB_ParticleSelection_vals_t, get_PCHB_weight



    type, extends(EnumBase_t) :: PCHB_ParticleSelection_t
        private
        character(9) :: str
    contains
        procedure :: to_str
    end type

    type :: PCHB_ParticleSelection_vals_t
        type(PCHB_ParticleSelection_t) :: &
            UNIF_UNIF = PCHB_ParticleSelection_t(1, 'UNIF-UNIF'), &
                !! Both particles are drawn uniformly.
            FULL_FULL = PCHB_ParticleSelection_t(2, 'FULL-FULL'), &
                !! We draw from \( p(I)|_{D_i} \) and then \( p(J | I)_{J \in D_i} \)
                !! and both probabilites come from the PCHB weighting scheme.
                !! We guarantee that \(I\) and \(J\) are occupied.
            UNIF_FULL = PCHB_ParticleSelection_t(3, 'UNIF-FULL'), &
                !! We draw \( \tilde{p}(I)|_{D_i} \) uniformly and then \( p(J | I)_{J \in D_i} \)
                !! The second distribution comes from the PCHB weighting scheme.
                !! We guarantee that \(I\) and \(J\) are occupied.
            UNIF_FAST = PCHB_ParticleSelection_t(4, 'UNIF-FAST')
                !! We draw \( \tilde{p}(I)|_{D_i} \) uniformly and then \( p(J | I)_{J} \).
                !! The second distribution comes from the PCHB weighting scheme.
                !! We guarantee that \(I\) is occupied.
    contains
        procedure, nopass :: from_str => from_keyword
    end type

    type(PCHB_ParticleSelection_vals_t), parameter :: &
        PCHB_particle_selection_vals = PCHB_ParticleSelection_vals_t()


    type, abstract :: ParticleSelector_t
    contains
        procedure(Draw_t), public, deferred :: draw
        procedure(GetPgen_t), public, deferred :: get_pgen
        procedure(Finalize_t), public, deferred :: finalize
        procedure(GetMemoryDemand_t), public, deferred :: get_memory_demand
    end type

    abstract interface
        subroutine Finalize_t(this)
            import :: ParticleSelector_t
            implicit none
            class(ParticleSelector_t), intent(inout) :: this
        end subroutine

        real(dp) pure function GetPgen_t(this, nI, i_sg, I, J)
            import :: dp, ParticleSelector_t, nEl
            implicit none
            class(ParticleSelector_t), intent(in) :: this
            integer, intent(in) :: nI(nEl), i_sg
                !! The determinant in nI-format and the prop_vec index
            integer, intent(in) :: I, J
                !! The particles.
        end function

        type(ByteSize_t) elemental function GetMemoryDemand_t(this)
            import :: ParticleSelector_t, ByteSize_t
            implicit none
            class(ParticleSelector_t), intent(in) :: this
        end function

        subroutine Draw_t(this, nI, ilutI, i_sg, elecs, srcs, p)
            import :: dp, ParticleSelector_t, nEl, n_int, nIfD
            class(ParticleSelector_t), intent(in) :: this
            integer, intent(in) :: nI(nEl), i_sg
                !! The determinant in nI-format and the prop_vec index
            integer(n_int), intent(in) :: ilutI(0 : nIfD)
                !! The determinant in bitmask format
            integer, intent(out) :: srcs(2), elecs(2)
                !! The chosen particles \(I, J\) and their index in `nI`.
                !! It is guaranteed that `scrs(1) < srcs(2)`.
            real(dp), intent(out) :: p
                !! The probability of drawing \( p(\{I, J\}) \Big |_{D_i} \).
                !! This is the probability of drawing two particles from
                !! a given determinant \(D_i\) regardless of order.
        end subroutine
    end interface

    type, extends(ParticleSelector_t) :: UniformParticles_t
    contains
        private
        procedure, public :: draw => draw_UniformParticles_t
        procedure, public :: get_pgen => get_pgen_UniformParticles_t
        procedure, public :: finalize => finalize_UniformParticles_t
        procedure, public :: get_memory_demand => get_memory_demand_UniformParticles_t
    end type

    type, extends(ParticleSelector_t), abstract :: PC_Particles_t
        private
        type(AliasSampler_1D_t) :: I_sampler
            !! The shape is (n_prop_vec) -> number_of_spin_orbs
        type(AliasSampler_2D_t) :: J_sampler
            !! The shape is (number_of_spin_orbs, n_prop_vec) -> number_of_spin_orbs

        class(PropertyIndexer_t), allocatable :: indexer
        logical, public :: use_lookup = .false.
            !! Use a lookup for the prop_vec index in global_det_data.
        logical, public :: create_lookup = .false.
            !! Create **and** manage! the prop_vec index lookup in global_det_data.
    contains
        private
        procedure, public :: init => init_PC_WeightedParticles_t
        procedure, public :: finalize => finalize_PC_WeightedParticles_t
        procedure, public :: get_memory_demand => get_memory_demand_PC_Particles_t
    end type

    type, extends(PC_Particles_t) :: PC_FullyWeightedParticles_t
    contains
        private
        procedure, public :: draw => draw_PC_FullyWeightedParticles_t
        procedure, public :: get_pgen => get_pgen_PC_FullyWeightedParticles_t
    end type

    type, extends(PC_Particles_t) :: PC_WeightedParticles_t
    contains
        private
        procedure, public :: draw => draw_PC_WeightedParticles_t
        procedure, public :: get_pgen => get_pgen_PC_WeightedParticles_t
    end type

    type, extends(PC_Particles_t) :: PC_FastWeightedParticles_t
    contains
        private
        procedure, public :: draw => draw_PC_FastWeightedParticles_t
        procedure, public :: get_pgen => get_pgen_PC_FastWeightedParticles_t
    end type

contains

    pure function to_str(options) result(res)
        class(PCHB_ParticleSelection_t), intent(in) :: options
        character(9) :: res
        res = options%str
    end function

    pure function from_keyword(w) result(res)
        !! Parse a given keyword into the possible particle selection schemes
        character(*), intent(in) :: w
        type(PCHB_ParticleSelection_t) :: res
        routine_name("from_keyword")
        select case(to_upper(w))
        case(PCHB_particle_selection_vals%UNIF_UNIF%str)
            res = PCHB_particle_selection_vals%UNIF_UNIF
        case(PCHB_particle_selection_vals%FULL_FULL%str)
            res = PCHB_particle_selection_vals%FULL_FULL
        case(PCHB_particle_selection_vals%UNIF_FULL%str)
            res = PCHB_particle_selection_vals%UNIF_FULL
        case(PCHB_particle_selection_vals%UNIF_FAST%str)
            res = PCHB_particle_selection_vals%UNIF_FAST
        case default
            call stop_all(this_routine, trim(w)//" not a valid doubles particle selection scheme.")
        end select
    end function

    subroutine allocate_and_init(PCHB_particle_selection, indexer, IJ_weights, rank_with_info, use_lookup, particle_selector)
        type(PCHB_ParticleSelection_t), intent(in) :: PCHB_particle_selection
        class(PropertyIndexer_t), intent(in) :: indexer
        real(dp), intent(in) :: IJ_weights(:, :, :)
        integer, intent(in) :: rank_with_info
            !! The **intra-node** rank that contains the weights to be used
            !! all other arr of all other ranks are ignored (and can be allocated with size 0).
        logical, intent(in) :: use_lookup
        class(ParticleSelector_t), allocatable, intent(inout) :: particle_selector
        routine_name("gasci_pchb_doubles_select_particles::allocate_and_init")
        if (PCHB_particle_selection == PCHB_particle_selection_vals%FULL_FULL) then
            allocate(PC_FullyWeightedParticles_t :: particle_selector)
            select type(particle_selector)
            type is(PC_FullyWeightedParticles_t)
                call particle_selector%init(indexer, IJ_weights, rank_with_info, use_lookup, .false.)
            end select
        else if (PCHB_particle_selection == PCHB_particle_selection_vals%UNIF_FULL) then
            allocate(PC_WeightedParticles_t :: particle_selector)
            select type(particle_selector)
            type is(PC_WeightedParticles_t)
                call particle_selector%init(indexer, IJ_weights, rank_with_info, use_lookup, .false.)
            end select
        else if (PCHB_particle_selection == PCHB_particle_selection_vals%UNIF_FAST) then
            allocate(PC_FastWeightedParticles_t :: particle_selector)
            select type(particle_selector)
            type is(PC_FastWeightedParticles_t)
                call particle_selector%init(indexer, IJ_weights, rank_with_info, use_lookup, .false.)
            end select
        else if (PCHB_particle_selection == PCHB_particle_selection_vals%UNIF_UNIF) then
            allocate(UniformParticles_t :: particle_selector)
        else
            call stop_all(this_routine, 'Invalid particle selection.')
        end if
    end subroutine

    subroutine draw_UniformParticles_t(this, nI, ilutI, i_sg, elecs, srcs, p)
        class(UniformParticles_t), intent(in) :: this
        integer, intent(in) :: nI(nEl), i_sg
            !! The determinant in nI-format and the prop_vec index
        integer(n_int), intent(in) :: ilutI(0 : nIfD)
            !! The determinant in bitmask format
        integer, intent(out) :: srcs(2), elecs(2)
            !! The chosen particles \(I, J\) and their index in `nI`.
            !! It is guaranteed that `scrs(1) < srcs(2)`.
        real(dp), intent(out) :: p
            !! The probability of drawing \( p(\{I, J\}) \Big |_{D_i} \).
            !! This is the probability of drawing two particles from
            !! a given determinant \(D_i\) regardless of order.
        integer :: sym_prod, ispn, sum_ml
#ifdef WARNING_WORKAROUND_
        associate(this => this); end associate
        associate(ilutI => ilutI); end associate
        associate(i_sg => i_sg); end associate
#endif
        call pick_biased_elecs(nI, elecs, srcs, sym_prod, ispn, sum_ml, p)
    end subroutine

    elemental function get_memory_demand_UniformParticles_t(this) result(res)
        class(UniformParticles_t), intent(in) :: this
        type(ByteSize_t) :: res
#ifdef WARNING_WORKAROUND_
        associate(this => this); end associate
#endif
        res = ByteSize_t(0_int64)
    end function

    pure function get_pgen_UniformParticles_t(this, nI, i_sg, I, J) result(p)
        !! Calculates \( p(\{I, J\}) \Big |_{D_i} \)
        !!
        !! This is the probability of drawing two particles from
        !! a given determinant \(D_i\) regardless of order.
        !!
        !! Note that the unordered probability is given by the ordered
        !! probability as:
        !! $$ p(\{I, J\}) \Big |_{D_i} = p((I, J)) \Big |_{D_i}
        !!              + p((J, I)) \Big |_{D_i} \quad.$$
        !! In addition we have
        !! $$ p((I, J)) \Big |_{D_i} \neq p((J, I)) \Big |_{D_i} $$
        !! so we have to actually calculate the probability of drawing
        !! two given particles in different order.
        class(UniformParticles_t), intent(in) :: this
        integer, intent(in) :: nI(nEl), i_sg
            !! The determinant in nI-format and the prop_vec index
        integer, intent(in) :: I, J
            !! The particles.
        real(dp) :: p
#ifdef WARNING_WORKAROUND_
        associate(this => this); end associate
        associate(nI => nI); end associate
        associate(i_sg => i_sg); end associate
#endif

        p = get_pgen_pick_biased_elecs(&
            is_beta(I) .eqv. is_beta(J), pParallel, &
            par_elec_pairs, AB_elec_pairs)
    end function

    subroutine finalize_UniformParticles_t(this)
        class(UniformParticles_t), intent(inout) :: this
#ifdef WARNING_WORKAROUND_
        associate(this => this); end associate
#endif
        ! Nothing to do
    end subroutine


    subroutine init_PC_WeightedParticles_t(this, indexer, weights, rank_with_info, use_lookup, create_lookup)
        class(PC_Particles_t), intent(inout) :: this
        class(PropertyIndexer_t), intent(in) :: indexer
        real(dp), intent(in) :: weights(:, :, :)
            !! `w(J, I, i_sg)`, The weight of picking `J` after picking `I` under prop_vec `i_sg`
        integer, intent(in) :: rank_with_info
            !! The **intra-node** rank that contains the weights to be used
            !! all other arr of all other ranks are ignored (and can be allocated with size 0).
        logical, intent(in) :: use_lookup, create_lookup
        character(*), parameter :: this_routine = 'init'

        integer, allocatable :: prop_vecs(:, :)
        integer :: nBI
        integer :: i_sg

        this%indexer = indexer
        this%create_lookup = create_lookup
        this%use_lookup = use_lookup

        if (this%create_lookup) then
            if (allocated(lookup_property_indexer)) then
                call stop_all(this_routine, 'Someone else is already managing the prop_vec lookup.')
            else
                write(stdout, *) 'PC particles is creating and managing the prop_vec lookup'
                allocate(lookup_property_indexer, source=this%indexer)
            end if
        end if
        if (this%use_lookup) write(stdout, *) 'PC particles is using the prop_vec lookup'

        nBI = nBasis

        prop_vecs = this%indexer%get_prop_vecs()

        if (iProcIndex_intra == rank_with_info) then
#ifdef DEBUG_
    block
        use util_mod, only: stop_all
        use constants, only: stderr
        if (.not. (nBI == size(weights, 1) .and. nBI == size(weights, 2))) then
            write(stderr, *) ""
            write(stderr, *) "Assertion nBI == size(weights, 1) .and. nBI == size(weights, 2)"
            write(stderr, *) "failed in /home/cicdadmin/fkfest-gh-runners/altest-runner-7/_work/NECI_autoforward/NECI_autoforward/s&
                &rc/property_vector_constraint/property_vector_pchb_doubles_select_particles.fpp:314"
            call stop_all (this_routine, "Assert fail: nBI == size(weights, 1) .and. nBI == size(weights, 2)")
        end if
    end block
#endif
#ifdef DEBUG_
    block
        use util_mod, only: stop_all
        use constants, only: stderr
        if (.not. (size(prop_vecs, 2) == size(weights, 3))) then
            write(stderr, *) ""
            write(stderr, *) "Assertion size(prop_vecs, 2) == size(weights, 3)"
            write(stderr, *) "failed in /home/cicdadmin/fkfest-gh-runners/altest-runner-7/_work/NECI_autoforward/NECI_autoforward/s&
                &rc/property_vector_constraint/property_vector_pchb_doubles_select_particles.fpp:315"
            call stop_all (this_routine, "Assert fail: size(prop_vecs, 2) == size(weights, 3)")
        end if
    end block
#endif
        end if

        call this%I_sampler%shared_alloc(size(prop_vecs, 2), nBI, 'PC_particles_i')
        call this%J_sampler%shared_alloc([nBI, size(prop_vecs, 2)], nBI, 'PC_particles_j')
        block
            integer :: I
            real(dp) :: dummy(2)
            dummy = 10000._dp
            do i_sg = 1, size(prop_vecs, 2)
                do I = 1, nBI
                    if (iProcIndex_intra == rank_with_info) then
                        call this%J_sampler%setup_entry(I, i_sg, rank_with_info, weights(:, I, i_sg))
                    else
                        call this%J_sampler%setup_entry(I, i_sg, rank_with_info, dummy)
                    end if
                end do
                if (iProcIndex_intra == rank_with_info) then
                    call this%I_sampler%setup_entry(i_sg, rank_with_info, sum(weights(:, :, i_sg), dim=1))
                else
                    call this%I_sampler%setup_entry(i_sg, rank_with_info, dummy)
                end if
            end do
        end block
    end subroutine

    elemental function get_memory_demand_PC_Particles_t(this) result(res)
        class(PC_Particles_t), intent(in) :: this
        type(ByteSize_t) :: res
        ! Strictly speaking this is not the whole memory demand.
        ! e.g. the gas specification is missing
        res = this%I_sampler%get_memory_demand() + this%J_sampler%get_memory_demand()
    end function

    subroutine draw_PC_FullyWeightedParticles_t(this, nI, ilutI, i_sg, elecs, srcs, p)
        class(PC_FullyWeightedParticles_t), intent(in) :: this
        integer, intent(in) :: nI(nEl), i_sg
            !! The determinant in nI-format and the prop_vec index
        integer(n_int), intent(in) :: ilutI(0 : nIfD)
            !! The determinant in bitmask format
        integer, intent(out) :: srcs(2), elecs(2)
            !! The chosen particles \(I, J\) and their index in `nI`.
            !! It is guaranteed that `scrs(1) < srcs(2)`.
        real(dp), intent(out) :: p
            !! The probability of drawing \( p(\{I, J\}) \Big |_{D_i} \).
            !! This is the probability of drawing two particles from
            !! a given determinant \(D_i\) regardless of order.
        character(*), parameter :: this_routine = 'draw'

        real(dp) :: renorm_first, p_first(2)
        real(dp) :: renorm_second(2), p_second(2)

        renorm_first = sum(this%I_sampler%get_prob(i_sg, nI))
        call this%I_sampler%constrained_sample(&
            i_sg, nI, ilutI, renorm_first, elecs(1), srcs(1), p_first(1))
#ifdef DEBUG_
    block
        use util_mod, only: stop_all
        use constants, only: stderr
        if (.not. (nI(elecs(1)) == srcs(1))) then
            write(stderr, *) ""
            write(stderr, *) "Assertion nI(elecs(1)) == srcs(1)"
            write(stderr, *) "failed in /home/cicdadmin/fkfest-gh-runners/altest-runner-7/_work/NECI_autoforward/NECI_autoforward/s&
                &rc/property_vector_constraint/property_vector_pchb_doubles_select_particles.fpp:370"
            call stop_all (this_routine, "Assert fail: nI(elecs(1)) == srcs(1)")
        end if
    end block
#endif

        renorm_second(1) = sum(this%J_sampler%get_prob(srcs(1), i_sg, nI))

        ! Note that p(I | I) is automatically zero and cannot be drawn
        call this%J_sampler%constrained_sample(&
            srcs(1), i_sg, nI, ilutI, renorm_second(1), elecs(2), srcs(2), p_second(1))
        if (srcs(2) == 0) then
            elecs(:) = 0; srcs(:) = 0; p = 1._dp
            return
        end if
#ifdef DEBUG_
    block
        use util_mod, only: stop_all
        use constants, only: stderr
        if (.not. (srcs(2) .in. nI)) then
            write(stderr, *) ""
            write(stderr, *) "Assertion srcs(2) .in. nI"
            write(stderr, *) "failed in /home/cicdadmin/fkfest-gh-runners/altest-runner-7/_work/NECI_autoforward/NECI_autoforward/s&
                &rc/property_vector_constraint/property_vector_pchb_doubles_select_particles.fpp:381"
            call stop_all (this_routine, "Assert fail: srcs(2) .in. nI")
        end if
    end block
#endif
#ifdef DEBUG_
    block
        use util_mod, only: stop_all
        use constants, only: stderr
        if (.not. (srcs(1) /= srcs(2))) then
            write(stderr, *) ""
            write(stderr, *) "Assertion srcs(1) /= srcs(2)"
            write(stderr, *) "failed in /home/cicdadmin/fkfest-gh-runners/altest-runner-7/_work/NECI_autoforward/NECI_autoforward/s&
                &rc/property_vector_constraint/property_vector_pchb_doubles_select_particles.fpp:382"
            call stop_all (this_routine, "Assert fail: srcs(1) /= srcs(2)")
        end if
    end block
#endif
#ifdef DEBUG_
    block
        use util_mod, only: stop_all
        use constants, only: stderr
        if (.not. (elecs(1) /= elecs(2))) then
            write(stderr, *) ""
            write(stderr, *) "Assertion elecs(1) /= elecs(2)"
            write(stderr, *) "failed in /home/cicdadmin/fkfest-gh-runners/altest-runner-7/_work/NECI_autoforward/NECI_autoforward/s&
                &rc/property_vector_constraint/property_vector_pchb_doubles_select_particles.fpp:383"
            call stop_all (this_routine, "Assert fail: elecs(1) /= elecs(2)")
        end if
    end block
#endif
#ifdef DEBUG_
    block
        use util_mod, only: stop_all
        use constants, only: stderr
        if (.not. (all(nI(elecs) == srcs))) then
            write(stderr, *) ""
            write(stderr, *) "Assertion all(nI(elecs) == srcs)"
            write(stderr, *) "failed in /home/cicdadmin/fkfest-gh-runners/altest-runner-7/_work/NECI_autoforward/NECI_autoforward/s&
                &rc/property_vector_constraint/property_vector_pchb_doubles_select_particles.fpp:384"
            call stop_all (this_routine, "Assert fail: all(nI(elecs) == srcs)")
        end if
    end block
#endif

        ! We could have picked them the other way round.
        ! Account for that.
        ! The renormalization for the first electron is the same
        p_first(2) = this%I_sampler%constrained_getProb(&
            i_sg, nI, renorm_first, srcs(2))
        renorm_second(2) = sum(this%J_sampler%get_prob(srcs(2), i_sg, nI))
        p_second(2) = this%J_sampler%constrained_getProb(&
            srcs(2), i_sg, nI, renorm_second(2), srcs(1))

        p = sum(p_first * p_second)

        if (srcs(1) > srcs(2)) then
            call swap(srcs(1), srcs(2))
            call swap(elecs(1), elecs(2))
        end if

#ifdef DEBUG_
    block
        use util_mod, only: stop_all
        use constants, only: stderr
        if (.not. (p .isclose. this%get_pgen(nI, i_sg, srcs(1), srcs(2)))) then
            write(stderr, *) ""
            write(stderr, *) "Assertion p .isclose. this%get_pgen(nI, i_sg, srcs(1), srcs(2))"
            write(stderr, *) "failed in /home/cicdadmin/fkfest-gh-runners/altest-runner-7/_work/NECI_autoforward/NECI_autoforward/s&
                &rc/property_vector_constraint/property_vector_pchb_doubles_select_particles.fpp:402"
            call stop_all (this_routine, "Assert fail: p .isclose. this%get_pgen(nI, i_sg, srcs(1), srcs(2))")
        end if
    end block
#endif
    end subroutine

    pure function get_pgen_PC_FullyWeightedParticles_t(this, nI, i_sg, I, J) result(p)
        !! Calculates \( p(\{I, J\}) \Big |_{D_i} \)
        !!
        !! This is the probability of drawing two particles from
        !! a given determinant \(D_i\) regardless of order.
        !!
        !! Note that the unordered probability is given by the ordered
        !! probability as:
        !! $$ p(\{I, J\}) \Big |_{D_i} = p((I, J)) \Big |_{D_i}
        !!              + p((J, I)) \Big |_{D_i} \quad.$$
        !! In addition we have
        !! $$ p((I, J)) \Big |_{D_i} \neq p((J, I)) \Big |_{D_i} $$
        !! so we have to actually calculate the probability of drawing
        !! two given particles in different order.
        class(PC_FullyWeightedParticles_t), intent(in) :: this
        integer, intent(in) :: nI(nEl), i_sg
            !! The determinant in nI-format and the prop_vec index
        integer, intent(in) :: I, J
            !! The particles.
        real(dp) :: p

        real(dp) :: renorm_first, p_first(2), renorm_second(2), p_second(2)

        ! The renormalization for the first electron is the same,
        ! regardless of order.
        renorm_first = sum(this%I_sampler%get_prob(i_sg, nI))

        p_first(1) = this%I_sampler%constrained_getProb(i_sg, nI, renorm_first, I)
        p_first(2) = this%I_sampler%constrained_getProb(i_sg, nI, renorm_first, J)

        renorm_second(1) = sum(this%J_sampler%get_prob(I, i_sg, nI))
        p_second(1) = this%J_sampler%constrained_getProb(&
            I, i_sg, nI, renorm_second(1), J)

        renorm_second(2) = sum(this%J_sampler%get_prob(J, i_sg, nI))
        p_second(2) = this%J_sampler%constrained_getProb(&
            J, i_sg, nI, renorm_second(2), I)

        p = sum(p_first * p_second)
    end function

    subroutine draw_PC_WeightedParticles_t(this, nI, ilutI, i_sg, elecs, srcs, p)
        class(PC_WeightedParticles_t), intent(in) :: this
        integer, intent(in) :: nI(nEl), i_sg
            !! The determinant in nI-format and the prop_vec index
        integer(n_int), intent(in) :: ilutI(0 : nIfD)
            !! The determinant in bitmask format
        integer, intent(out) :: srcs(2), elecs(2)
            !! The chosen particles \(I, J\) and their index in `nI`.
            !! It is guaranteed that `scrs(1) < srcs(2)`.
        real(dp), intent(out) :: p
            !! The probability of drawing \( p(\{I, J\}) \Big |_{D_i} \).
            !! This is the probability of drawing two particles from
            !! a given determinant \(D_i\) regardless of order.
        character(*), parameter :: this_routine = 'draw'
        real(dp) :: renorm_second(2), p_second(2)

        elecs(1) = int(genrand_real2_dSFMT() * nEl) + 1
        srcs(1) = nI(elecs(1))

        renorm_second(1) = sum(this%J_sampler%get_prob(srcs(1), i_sg, nI))
        ! Note that p(I | I) is automatically zero and cannot be drawn
        call this%J_sampler%constrained_sample(&
            srcs(1), i_sg, nI, ilutI, renorm_second(1), elecs(2), srcs(2), p_second(1))
        if (srcs(2) == 0) then
            elecs(:) = 0; srcs(:) = 0; p = 1._dp
            return
        end if

#ifdef DEBUG_
    block
        use util_mod, only: stop_all
        use constants, only: stderr
        if (.not. ((srcs(1) .in. nI) .and. (srcs(2) .in. nI))) then
            write(stderr, *) ""
            write(stderr, *) "Assertion (srcs(1) .in. nI) .and. (srcs(2) .in. nI)"
            write(stderr, *) "failed in /home/cicdadmin/fkfest-gh-runners/altest-runner-7/_work/NECI_autoforward/NECI_autoforward/s&
                &rc/property_vector_constraint/property_vector_pchb_doubles_select_particles.fpp:474"
            call stop_all (this_routine, "Assert fail: (srcs(1) .in. nI) .and. (srcs(2) .in. nI)")
        end if
    end block
#endif
#ifdef DEBUG_
    block
        use util_mod, only: stop_all
        use constants, only: stderr
        if (.not. (srcs(1) /= srcs(2))) then
            write(stderr, *) ""
            write(stderr, *) "Assertion srcs(1) /= srcs(2)"
            write(stderr, *) "failed in /home/cicdadmin/fkfest-gh-runners/altest-runner-7/_work/NECI_autoforward/NECI_autoforward/s&
                &rc/property_vector_constraint/property_vector_pchb_doubles_select_particles.fpp:475"
            call stop_all (this_routine, "Assert fail: srcs(1) /= srcs(2)")
        end if
    end block
#endif
#ifdef DEBUG_
    block
        use util_mod, only: stop_all
        use constants, only: stderr
        if (.not. (elecs(1) /= elecs(2))) then
            write(stderr, *) ""
            write(stderr, *) "Assertion elecs(1) /= elecs(2)"
            write(stderr, *) "failed in /home/cicdadmin/fkfest-gh-runners/altest-runner-7/_work/NECI_autoforward/NECI_autoforward/s&
                &rc/property_vector_constraint/property_vector_pchb_doubles_select_particles.fpp:476"
            call stop_all (this_routine, "Assert fail: elecs(1) /= elecs(2)")
        end if
    end block
#endif
#ifdef DEBUG_
    block
        use util_mod, only: stop_all
        use constants, only: stderr
        if (.not. (all(nI(elecs) == srcs))) then
            write(stderr, *) ""
            write(stderr, *) "Assertion all(nI(elecs) == srcs)"
            write(stderr, *) "failed in /home/cicdadmin/fkfest-gh-runners/altest-runner-7/_work/NECI_autoforward/NECI_autoforward/s&
                &rc/property_vector_constraint/property_vector_pchb_doubles_select_particles.fpp:477"
            call stop_all (this_routine, "Assert fail: all(nI(elecs) == srcs)")
        end if
    end block
#endif

        ! We could have picked them the other way round.
        ! Account for that.
        ! The renormalization for the first electron is the same
        renorm_second(2) = sum(this%J_sampler%get_prob(srcs(2), i_sg, nI))
        p_second(2) = this%J_sampler%constrained_getProb(&
            srcs(2), i_sg, nI, renorm_second(2), srcs(1))

        p = sum(p_second) / nEl

        if (srcs(1) > srcs(2)) then
            call swap(srcs(1), srcs(2))
            call swap(elecs(1), elecs(2))
        end if

#ifdef DEBUG_
    block
        use util_mod, only: stop_all
        use constants, only: stderr
        if (.not. (p .isclose. this%get_pgen(nI, i_sg, srcs(1), srcs(2)))) then
            write(stderr, *) ""
            write(stderr, *) "Assertion p .isclose. this%get_pgen(nI, i_sg, srcs(1), srcs(2))"
            write(stderr, *) "failed in /home/cicdadmin/fkfest-gh-runners/altest-runner-7/_work/NECI_autoforward/NECI_autoforward/s&
                &rc/property_vector_constraint/property_vector_pchb_doubles_select_particles.fpp:493"
            call stop_all (this_routine, "Assert fail: p .isclose. this%get_pgen(nI, i_sg, srcs(1), srcs(2))")
        end if
    end block
#endif
    end subroutine

    pure function get_pgen_PC_WeightedParticles_t(this, nI, i_sg, I, J) result(p)
        !! Calculates \( p(\{I, J\}) \Big |_{D_i} \)
        !!
        !! This is the probability of drawing two particles from
        !! a given determinant \(D_i\) regardless of order.
        !!
        !! Note that the unordered probability is given by the ordered
        !! probability as:
        !! $$ p(\{I, J\}) \Big |_{D_i} = p((I, J)) \Big |_{D_i}
        !!              + p((J, I)) \Big |_{D_i} \quad.$$
        !! In addition we have
        !! $$ p((I, J)) \Big |_{D_i} \neq p((J, I)) \Big |_{D_i} $$
        !! so we have to actually calculate the probability of drawing
        !! two given particles in different order.
        class(PC_WeightedParticles_t), intent(in) :: this
        integer, intent(in) :: nI(nEl), i_sg
            !! The determinant in nI-format and the prop_vec index
        integer, intent(in) :: I, J
            !! The particles.
        real(dp) :: p

        real(dp) :: renorm_second(2), p_second(2)

        ! The probability for the first electron is always 1 / nEl

        renorm_second(1) = sum(this%J_sampler%get_prob(I, i_sg, nI))
        p_second(1) = this%J_sampler%constrained_getProb(&
            I, i_sg, nI, renorm_second(1), J)

        renorm_second(2) = sum(this%J_sampler%get_prob(J, i_sg, nI))
        p_second(2) = this%J_sampler%constrained_getProb(&
            J, i_sg, nI, renorm_second(2), I)

        p = sum(p_second) / nEl
    end function



    subroutine draw_PC_FastWeightedParticles_t(this, nI, ilutI, i_sg, elecs, srcs, p)
        class(PC_FastWeightedParticles_t), intent(in) :: this
        integer, intent(in) :: nI(nEl), i_sg
            !! The determinant in nI-format and the prop_vec index
        integer(n_int), intent(in) :: ilutI(0 : nIfD)
            !! The determinant in bitmask format
        integer, intent(out) :: srcs(2), elecs(2)
            !! The chosen particles \(I, J\) and their index in `nI`.
            !! It is guaranteed that `scrs(1) < srcs(2)`.
        real(dp), intent(out) :: p
            !! The probability of drawing \( p(\{I, J\}) \Big |_{D_i} \).
            !! This is the probability of drawing two particles from
            !! a given determinant \(D_i\) regardless of order.
        character(*), parameter :: this_routine = 'draw'
        real(dp) :: p_J_I, p_I_J

        elecs(1) = int(genrand_real2_dSFMT() * nEl) + 1
        srcs(1) = nI(elecs(1))

        call this%J_sampler%sample(srcs(1), i_sg, srcs(2), p_J_I)

        if (srcs(2) == 0) then
            elecs(:) = 0; srcs(:) = 0; p = 1._dp
        else if (.not. IsOcc(ilutI, srcs(2))) then
            elecs(:) = 0; srcs(:) = 0; p = 1._dp
        else
            elecs(2) = int(binary_search_int(nI, srcs(2)))
#ifdef DEBUG_
    block
        use util_mod, only: stop_all
        use constants, only: stderr
        if (.not. ((srcs(1) .in. nI) .and. (srcs(2) .in. nI))) then
            write(stderr, *) ""
            write(stderr, *) "Assertion (srcs(1) .in. nI) .and. (srcs(2) .in. nI)"
            write(stderr, *) "failed in /home/cicdadmin/fkfest-gh-runners/altest-runner-7/_work/NECI_autoforward/NECI_autoforward/s&
                &rc/property_vector_constraint/property_vector_pchb_doubles_select_particles.fpp:561"
            call stop_all (this_routine, "Assert fail: (srcs(1) .in. nI) .and. (srcs(2) .in. nI)")
        end if
    end block
#endif
#ifdef DEBUG_
    block
        use util_mod, only: stop_all
        use constants, only: stderr
        if (.not. (srcs(1) /= srcs(2))) then
            write(stderr, *) ""
            write(stderr, *) "Assertion srcs(1) /= srcs(2)"
            write(stderr, *) "failed in /home/cicdadmin/fkfest-gh-runners/altest-runner-7/_work/NECI_autoforward/NECI_autoforward/s&
                &rc/property_vector_constraint/property_vector_pchb_doubles_select_particles.fpp:562"
            call stop_all (this_routine, "Assert fail: srcs(1) /= srcs(2)")
        end if
    end block
#endif
#ifdef DEBUG_
    block
        use util_mod, only: stop_all
        use constants, only: stderr
        if (.not. (elecs(1) /= elecs(2))) then
            write(stderr, *) ""
            write(stderr, *) "Assertion elecs(1) /= elecs(2)"
            write(stderr, *) "failed in /home/cicdadmin/fkfest-gh-runners/altest-runner-7/_work/NECI_autoforward/NECI_autoforward/s&
                &rc/property_vector_constraint/property_vector_pchb_doubles_select_particles.fpp:563"
            call stop_all (this_routine, "Assert fail: elecs(1) /= elecs(2)")
        end if
    end block
#endif
#ifdef DEBUG_
    block
        use util_mod, only: stop_all
        use constants, only: stderr
        if (.not. (all(nI(elecs) == srcs))) then
            write(stderr, *) ""
            write(stderr, *) "Assertion all(nI(elecs) == srcs)"
            write(stderr, *) "failed in /home/cicdadmin/fkfest-gh-runners/altest-runner-7/_work/NECI_autoforward/NECI_autoforward/s&
                &rc/property_vector_constraint/property_vector_pchb_doubles_select_particles.fpp:564"
            call stop_all (this_routine, "Assert fail: all(nI(elecs) == srcs)")
        end if
    end block
#endif


            p_I_J = this%J_sampler%get_prob(srcs(2), i_sg, srcs(1))
            p = (p_J_I + p_I_J) / nEl

            if (srcs(1) > srcs(2)) then
                call swap(srcs(1), srcs(2))
                call swap(elecs(1), elecs(2))
            end if

#ifdef DEBUG_
    block
        use util_mod, only: stop_all
        use constants, only: stderr
        if (.not. (p .isclose. this%get_pgen(nI, i_sg, srcs(1), srcs(2)))) then
            write(stderr, *) ""
            write(stderr, *) "Assertion p .isclose. this%get_pgen(nI, i_sg, srcs(1), srcs(2))"
            write(stderr, *) "failed in /home/cicdadmin/fkfest-gh-runners/altest-runner-7/_work/NECI_autoforward/NECI_autoforward/s&
                &rc/property_vector_constraint/property_vector_pchb_doubles_select_particles.fpp:575"
            call stop_all (this_routine, "Assert fail: p .isclose. this%get_pgen(nI, i_sg, srcs(1), srcs(2))")
        end if
    end block
#endif
        end if
    end subroutine

    pure function get_pgen_PC_FastWeightedParticles_t(this, nI, i_sg, I, J) result(p)
        !! Calculates \( p(\{I, J\}) \Big |_{D_i} \)
        !!
        !! This is the probability of drawing two particles from
        !! a given determinant \(D_i\) regardless of order.
        !!
        !! Note that the unordered probability is given by the ordered
        !! probability as:
        !! $$ p(\{I, J\}) \Big |_{D_i} = p((I, J)) \Big |_{D_i}
        !!              + p((J, I)) \Big |_{D_i} \quad.$$
        !! In addition we have
        !! $$ p((I, J)) \Big |_{D_i} \neq p((J, I)) \Big |_{D_i} $$
        !! so we have to actually calculate the probability of drawing
        !! two given particles in different order.
        class(PC_FastWeightedParticles_t), intent(in) :: this
        integer, intent(in) :: nI(nEl), i_sg
            !! The determinant in nI-format and the prop_vec index
        integer, intent(in) :: I, J
            !! The particles.
        real(dp) :: p

#ifdef WARNING_WORKAROUND_
        associate(nI => nI); end associate
#endif
        p = (this%J_sampler%get_prob(I, i_sg, J) &
              + this%J_sampler%get_prob(J, i_sg, I)) / nEl
    end function


    subroutine finalize_PC_WeightedParticles_t(this)
        class(PC_Particles_t), intent(inout) :: this

        if (allocated(this%indexer)) then
            call this%I_sampler%finalize()
            call this%J_sampler%finalize()
            ! Yes, we assume, that either all or none are allocated
            deallocate(this%indexer)
            if (this%create_lookup) deallocate(lookup_property_indexer)
        end if
    end subroutine


    function get_PCHB_weight(exc) result(res)
        !! If there are three-body excitations,
        !! the double excitations actually become determinant-dependent.
        !! This function returns a fake determinant independent value in all
        !! cases, but evaluating `exc` for the reference determinant.
        type(Excite_2_t), intent(in) :: exc
        real(dp) :: res
        if (t_mol_3_body) then
            res = abs(sltcnd_excit(projEDet(:, 1), exc, .false., assert_occupation=.false.))
        else
            res = abs(nI_invariant_sltcnd_excit(exc))
        end if
    end function

end module
