module procedures_short_interactions_visitor

use, intrinsic :: iso_fortran_env, only: DP => REAL64
use types_logical_wrapper, only: Logical_Rectangle
use procedures_visit_condition, only: abstract_visit_condition, visit_different, visit_lower, &
    visit_all
use types_particle_wrapper, only: Concrete_Particle
use types_component_wrapper, only: Component_Wrapper
use classes_pair_potential, only: Pair_Potential_Wrapper, Pair_Potential_Line
use classes_walls_visitor, only: Abstract_Walls_Visitor
use classes_short_pairs_visitor, only: Abstract_Short_Pairs_Visitor
use classes_visitable_cells, only: Abstract_Visitable_Cells
use types_real_wrapper, only: Real_Line

implicit none

private
public :: visit

interface visit
    module procedure :: visit_walls
    module procedure :: visit_short
    module procedure :: visit_cells_energies
    module procedure :: visit_cells_contacts
    module procedure :: visit_cells_min_distance
    module procedure :: visit_dipolar_neighbours
end interface visit

contains

    pure subroutine visit_walls(overlap, energies, components, walls_visitors, wall_pairs)
        logical, intent(out) :: overlap
        real(DP), intent(inout) :: energies(:)
        type(Component_Wrapper), intent(in) :: components(:)
        class(Abstract_Walls_Visitor), intent(in) :: walls_visitors
        type(Pair_Potential_Wrapper), intent(in) :: wall_pairs(:)

        integer :: i_component

        overlap = .false.
        do i_component = 1, size(energies)
            call walls_visitors%visit(overlap, energies(i_component), &
                components(i_component)%positions, wall_pairs(i_component)%potential)
            if (overlap) return
        end do
    end subroutine visit_walls

    pure subroutine visit_short(overlap, energies, components, components_visitor, components_pairs)
        logical, intent(out) :: overlap
        type(Real_Line), intent(inout) :: energies(:)
        type(Component_Wrapper), intent(in) :: components(:)
        class(Abstract_Short_Pairs_Visitor), intent(in) :: components_visitor
        type(Pair_Potential_Line), intent(in) :: components_pairs(:)

        call visit_short_intra(overlap, energies, components, components_visitor, components_pairs)
        if (overlap) return
        call visit_short_inter(overlap, energies, components, components_visitor, components_pairs)
    end subroutine visit_short

    pure subroutine visit_short_intra(overlap, energies, components, components_visitor, &
        components_pairs)
        logical, intent(out) :: overlap
        type(Real_Line), intent(inout) :: energies(:)
        type(Component_Wrapper), intent(in) :: components(:)
        class(Abstract_Short_Pairs_Visitor), intent(in) :: components_visitor
        type(Pair_Potential_Line), intent(in) :: components_pairs(:)

        integer :: i_component

        overlap = .false.
        do i_component = 1, size(components)
            associate(energy_i => energies(i_component)%line(i_component), &
                positions_i => components(i_component)%positions, &
                potential_i => components_pairs(i_component)%line(i_component)%potential)
            call components_visitor%visit(overlap, energy_i, positions_i, potential_i)
            end associate
            if (overlap) return
        end do
    end subroutine visit_short_intra

    pure subroutine visit_short_inter(overlap, energies, components, components_visitor, &
        components_pairs)
        logical, intent(out) :: overlap
        type(Real_Line), intent(inout) :: energies(:)
        type(Component_Wrapper), intent(in) :: components(:)
        class(Abstract_Short_Pairs_Visitor), intent(in) :: components_visitor
        type(Pair_Potential_Line), intent(in) :: components_pairs(:)

        integer :: i_component, j_component

        overlap = .false.
        do j_component = 1, size(components)
            do i_component = 1, j_component - 1
                associate(energy_ij => energies(j_component)%line(i_component), &
                    positions_i => components(i_component)%positions, &
                    positions_j => components(j_component)%positions, &
                    potential_ij => components_pairs(j_component)%line(i_component)%potential)
                    call components_visitor%visit(overlap, energy_ij, positions_i, positions_j, &
                        potential_ij)
                end associate
                if (overlap) return
            end do
        end do
    end subroutine visit_short_inter

    subroutine visit_cells_energies(overlap, energies, components, visitable_cells)
        logical, intent(out) :: overlap
        type(Real_Line), intent(inout) :: energies(:)
        type(Component_Wrapper), intent(in) :: components(:)
        class(Abstract_Visitable_Cells), intent(in) :: visitable_cells(:, :)

        real(DP) :: energy_ij, energy_j
        integer :: i_component, j_component, i_particle, i_exclude
        logical :: same_component
        type(Concrete_Particle) :: particle
        procedure(abstract_visit_condition), pointer :: visit_condition => null()

        overlap = .false.
        do j_component = 1, size(energies)
            do i_component = 1, j_component
                same_component = i_component == j_component
                if (same_component) then
                    visit_condition => visit_lower
                else
                    visit_condition => visit_all
                end if

                energy_ij = 0._DP
                do i_particle = 1, components(j_component)%positions%get_num()
                    particle%i = i_particle
                    particle%position = components(j_component)%positions%get(particle%i)
                    i_exclude = merge(particle%i, 0, same_component)
                    call visitable_cells(i_component, j_component)%&
                        visit_energy(overlap, energy_j, particle, visit_condition, i_exclude)
                    if (overlap) return
                    energy_ij = energy_ij + energy_j
                end do
                energies(j_component)%line(i_component) = energy_ij
            end do
        end do
    end subroutine visit_cells_energies

    subroutine visit_cells_contacts(overlap, contacts, components, visitable_cells)
        logical, intent(out) :: overlap
        real(DP), intent(out) :: contacts
        type(Component_Wrapper), intent(in) :: components(:)
        class(Abstract_Visitable_Cells), intent(in) :: visitable_cells(:, :)

        real(DP) :: conctacts_j
        integer :: i_component, j_component, i_particle, i_exclude
        logical :: same_component
        type(Concrete_Particle) :: particle
        procedure(abstract_visit_condition), pointer :: visit_condition => null()

        overlap = .false.
        contacts = 0._DP
        do j_component = 1, size(visitable_cells, 2)
            do i_component = 1, j_component
                same_component = i_component == j_component
                if (same_component) then
                    visit_condition => visit_lower
                else
                    visit_condition => visit_all
                end if

                do i_particle = 1, components(j_component)%positions%get_num()
                    particle%i = i_particle
                    particle%position = components(j_component)%positions%get(particle%i)
                    i_exclude = merge(particle%i, 0, same_component)
                    call visitable_cells(i_component, j_component)%&
                        visit_contacts(overlap, conctacts_j, particle, visit_condition, i_exclude)
                    if (overlap) return
                    contacts = contacts + conctacts_j
                end do
            end do
        end do
    end subroutine visit_cells_contacts

    subroutine visit_cells_min_distance(overlap, min_distance_ratio, max_distance_ratio, &
        components, visitable_cells)
        logical, intent(out) :: overlap
        real(DP), intent(out) :: min_distance_ratio
        real(DP), intent(in) :: max_distance_ratio
        type(Component_Wrapper), intent(in) :: components(:)
        class(Abstract_Visitable_Cells), intent(in) :: visitable_cells(:, :)

        real(DP) :: min_distance_ratio_j
        integer :: i_component, j_component, i_particle, i_exclude
        logical :: same_component
        type(Concrete_Particle) :: particle
        procedure(abstract_visit_condition), pointer :: visit_condition => null()

        overlap = .false.
        min_distance_ratio = max_distance_ratio
        do j_component = 1, size(components)
            do i_component = 1, j_component
                same_component = i_component == j_component
                if (same_component) then
                    visit_condition => visit_lower
                else
                    visit_condition => visit_all
                end if

                do i_particle = 1, components(j_component)%positions%get_num()
                    particle%i = i_particle
                    particle%position = components(j_component)%positions%get(particle%i)
                    i_exclude = merge(particle%i, 0, same_component)
                    call visitable_cells(i_component, j_component)%&
                        visit_min_distance(overlap, min_distance_ratio_j, particle, &
                            visit_condition, i_exclude)
                    if (overlap) return
                    if (min_distance_ratio_j < min_distance_ratio) &
                        min_distance_ratio = min_distance_ratio_j
                end do
            end do
        end do
    end subroutine visit_cells_min_distance

    !> @note particle%dipole_moment is useless for now (cf. underlying implementation)
    subroutine visit_dipolar_neighbours(overlap, adjacency_matrices, components, visitable_cells)
        logical, intent(out) :: overlap
        type(Logical_Rectangle), intent(inout) :: adjacency_matrices(:, :)
        type(Component_Wrapper), intent(in) :: components(:)
        class(Abstract_Visitable_Cells), intent(in) :: visitable_cells(:, :)

        integer :: i_component, j_component, nums_dipoles(size(components))
        integer :: i_particle, i_exclude
        logical :: same_component
        type(Concrete_Particle) :: particle
        procedure(abstract_visit_condition), pointer :: visit_condition => null()

        do i_component = 1, size(nums_dipoles)
            nums_dipoles(i_component) = components(i_component)%orientations%get_num()
        end do

        overlap = .false.
        do j_component = 1, size(visitable_cells, 2)
            do i_component = 1, size(visitable_cells, 1)
                if (nums_dipoles(i_component) == 0 .or. nums_dipoles(j_component) == 0) then
                    adjacency_matrices(i_component, j_component)%rectangle = .false.
                    cycle
                end if

                same_component = i_component == j_component
                if (same_component) then
                    visit_condition => visit_different
                else
                    visit_condition => visit_all
                end if

                do i_particle = 1, components(j_component)%orientations%get_num()
                    particle%i = i_particle
                    particle%position = components(j_component)%positions%get(particle%i)
                    particle%orientation = components(j_component)%orientations%get(particle%i)
                    i_exclude = merge(particle%i, 0, same_component)
                    call visitable_cells(i_component, j_component)%&
                        visit_dipolar_neighbours(overlap, &
                            adjacency_matrices(i_component, j_component)%rectangle, particle, &
                            visit_condition, i_exclude)
                    if (overlap) return
                end do
            end do
        end do
    end subroutine visit_dipolar_neighbours

end module procedures_short_interactions_visitor
