module mod_load_balance
#ifdef USE_MPIWRAPPERS
  use mod_mpi_wrapper
#else
#define mpi_irecv_wrapper MPI_IRECV
#define mpi_isend_wrapper MPI_ISEND
#endif
  implicit none
  private
  !> MPI recv send variables for AMR.  One request per neighbouring rank, so
  !> these are sized by npe.
  integer :: itag, irecv, isend
  integer :: recv_igrid, recv_ipe, send_igrid, send_ipe, igrid
  integer, dimension(:), allocatable :: recvrequest, sendrequest
  integer, dimension(:,:), allocatable :: recvstatus, sendstatus
  !> MPI recv send variables for staggered-variable AMR
  integer, dimension(:), allocatable :: recvrequest_stg, sendrequest_stg

  !> MPI buffers to send blocks.
  !>
  !> The migration is aggregated per destination rank: every block bound for
  !> one peer occupies a contiguous run of snd_buff_lb and travels in a single
  !> Isend.  Both ranks order a run by the key
  !>
  !>     ikey = recv_igrid
  !>
  !> the block index the block will occupy on its new owner.  That is unique
  !> among the blocks arriving at a given receiver, because each comes from its
  !> own getnode(), and the sender knows it because load_balance walks the
  !> globally replicated Morton list in the same order on every rank and every
  !> rank calls getnode() for every migration - so igrid_inuse, and with it the
  !> destination index, stays replicated.
  !>
  !> Sizing matters more here than the message count.  These buffers used to be
  !> block_nx^3 * nw * max_blocks doubles each, allocated on the first call
  !> whether or not a single block ever moved: 1.05 GB apiece at block_nx=16,
  !> nw=8, max_blocks=4000, and device-resident.  They are now sized by what
  !> actually migrates, and only ever grow, so the steady state neither wastes
  !> the memory nor reallocates every regrid.
  !>
  !> Deliberately NOT declare create: these are resized, and an OpenACC
  !> declare-create allocatable does not reliably get a fresh device copy when
  !> the host array is reallocated - an update device afterwards then writes
  !> through a stale mapping.  They are managed explicitly with enter data
  !> create / exit data delete instead, the same way mod_fix_conserve handles
  !> its own resizable exchange buffers.
  double precision, allocatable, dimension(:) :: snd_buff_lb, rcv_buff_lb
  !> high-water mark of the two buffers, in chunks
  integer :: max_buff = 0

  !> Per migrating block, appended during the walk and turned into per-peer
  !> runs by exchange_migrated_blocks.  lb_snd_igrid / lb_snd_ibuf and the rcv
  !> counterparts are read by the pack and unpack kernels and are therefore
  !> device-resident; the rest is the host-side working set.
  integer :: n_lb_snd, n_lb_rcv
  integer, allocatable, dimension(:) :: lb_snd_igrid, lb_snd_ibuf
  integer, allocatable, dimension(:) :: lb_snd_dest, lb_snd_key
  integer, allocatable, dimension(:) :: lb_rcv_igrid, lb_rcv_ibuf
  integer, allocatable, dimension(:) :: lb_rcv_src, lb_rcv_key
  

  !> The peers themselves, ascending, with the extent of each one's run.  Host
  !> only: these drive the MPI calls and nothing else.
  integer :: n_lb_send_pe, n_lb_recv_pe
  integer, allocatable, dimension(:) :: lb_send_pe, lb_send_pe_off,&
     lb_send_pe_len
  integer, allocatable, dimension(:) :: lb_recv_pe, lb_recv_pe_off,&
     lb_recv_pe_len

  public :: load_balance

contains
  !> reallocate blocks into processors for load balance
  subroutine load_balance
    use mod_forest
    use mod_global_parameters
    use mod_space_filling_curve
    use mod_amr_solution_node, only: getnode,putnode
    use mod_functions_forest, only: change_ipe_tree_leaf

    integer :: Morton_no, ipe
    !> MPI recv send variables for staggered-variable AMR
    integer :: itag_stg
    integer, dimension(:,:), allocatable :: recvstatus_stg, sendstatus_stg

    ! Jannis: for now, not using version for passive/active blocks
    call get_Morton_range()

    if (npe==1) then
       sfc_to_igrid(:)=sfc(1,Morton_start(mype):Morton_stop(mype))
       return
    end if

    irecv=0
    isend=0
    n_lb_snd=0
    n_lb_rcv=0
    if (.not.allocated(recvrequest)) then
       allocate(recvstatus(MPI_STATUS_SIZE,npe),recvrequest(npe),&
           sendstatus(MPI_STATUS_SIZE,npe),sendrequest(npe))
       allocate(lb_send_pe(npe),lb_send_pe_off(npe),lb_send_pe_len(npe),&
           lb_recv_pe(npe),lb_recv_pe_off(npe),lb_recv_pe_len(npe))
    end if
    recvrequest=MPI_REQUEST_NULL
    sendrequest=MPI_REQUEST_NULL

    if(stagger_grid) then
      allocate(recvstatus_stg(MPI_STATUS_SIZE,max_blocks*3),&
         recvrequest_stg(max_blocks*3), sendstatus_stg(MPI_STATUS_SIZE,&
         max_blocks*3),sendrequest_stg(max_blocks*3))
      recvrequest_stg=MPI_REQUEST_NULL
      sendrequest_stg=MPI_REQUEST_NULL
    end if

    ! Only the descriptors are allocated up front, and they are integers.  The
    ! data buffers are sized by what actually migrates, in
    ! exchange_migrated_blocks, once the walk below has counted it.
    if ( .not. allocated(lb_snd_igrid) ) then
       allocate( lb_snd_igrid(max_blocks), lb_snd_ibuf(max_blocks), &
            lb_snd_dest(max_blocks), lb_snd_key(max_blocks), &
            lb_rcv_igrid(max_blocks), lb_rcv_ibuf(max_blocks), &
            lb_rcv_src(max_blocks), lb_rcv_key(max_blocks) )
    end if

    do ipe=0,npe-1; do Morton_no=Morton_start(ipe),Morton_stop(ipe)
       recv_ipe=ipe

       send_igrid=sfc(1,Morton_no)
       send_ipe=sfc(2,Morton_no)

       if (recv_ipe/=send_ipe) then
          ! get an igrid number for the new node in recv_ipe processor
          recv_igrid=getnode(recv_ipe)
          ! update node igrid and ipe on the tree
          call change_ipe_tree_leaf(recv_igrid,recv_ipe,send_igrid,send_ipe)
          ! receive physical data of the new node in recv_ipe processor
          if (recv_ipe==mype) call lb_recv
          ! send physical data of the old node in send_ipe processor
          if (send_ipe==mype) call lb_send
       end if
       if (recv_ipe==mype) then
          if (recv_ipe==send_ipe) then
             sfc_to_igrid(Morton_no)=send_igrid
          else
             sfc_to_igrid(Morton_no)=recv_igrid
          end if
       end if
    end do; end do

    ! Lay the blocks out per peer, exchange them in one message each, and apply
    ! them.  The walk above only recorded descriptors.
    call exchange_migrated_blocks

    ! The staggered path still posts per block; see the note in lb_send.  It is
    ! unreachable here (stagger_grid is false and fix_edges is not called), so
    ! it is left as it was found.
    if(stagger_grid) then
      if (irecv>0) call MPI_WAITALL(irecv,recvrequest_stg,recvstatus_stg,&
         ierrmpi)
      if (isend>0) call MPI_WAITALL(isend,sendrequest_stg,sendstatus_stg,&
         ierrmpi)
   end if

    if(stagger_grid) deallocate(recvstatus_stg,recvrequest_stg,sendstatus_stg,&
       sendrequest_stg)

    ! post processing
    do ipe=0,npe-1; do Morton_no=Morton_start(ipe),Morton_stop(ipe)
       recv_ipe=ipe

       send_igrid=sfc(1,Morton_no)
       send_ipe=sfc(2,Morton_no)

       if (recv_ipe/=send_ipe) then
          !if (send_ipe==mype) call dealloc_node(send_igrid)
          call putnode(send_igrid,send_ipe)
       end if
    end do; end do


    ! Update sfc array: igrid and ipe info in space filling curve
    call amr_Morton_order()

  end subroutine load_balance

  !> Exchange and apply the migrating blocks, one message per neighbouring
  !> rank, with the buffers sized by what actually moves.
  !>
  !> The walk in load_balance has already allocated every arriving block with
  !> alloc_node and recorded a descriptor for each departure and arrival. Here
  !> those are laid out as one contiguous run per peer, ordered by the shared
  !> key, packed by a single kernel and sent as a single message each.
  subroutine exchange_migrated_blocks
    use mod_global_parameters
    use mod_msg_layout, only: layout_runs
    use mod_comm_lib, only: mpistop

    integer :: k, nchunk, nbuf, nneed
    integer :: igrid, ibuf, iw, ix1, ix2, ix3
    integer, allocatable :: chunksize(:)

    ! Only 1:nwgc travels. The analytic extras past it - for ffhd the frozen
    ! field - were already rebuilt by fill_nwextra_device inside the
    ! alloc_node that lb_recv calls for every arriving block, so putting them
    ! on the wire would only overwrite identical values.
    nchunk = block_nx1*block_nx2*block_nx3*nwgc

    ! Every chunk is one block interior, so they are all the same size.
    allocate(chunksize(max(n_lb_snd,n_lb_rcv,1)))
    chunksize = nchunk
    call layout_runs(n_lb_snd, lb_snd_dest, lb_snd_key, chunksize, lb_snd_ibuf,&
       n_lb_send_pe, lb_send_pe, lb_send_pe_off, lb_send_pe_len)
    call layout_runs(n_lb_rcv, lb_rcv_src, lb_rcv_key, chunksize, lb_rcv_ibuf,&
       n_lb_recv_pe, lb_recv_pe, lb_recv_pe_off, lb_recv_pe_len)
    deallocate(chunksize)

    ! Grow the buffers to fit, never shrink: load_balance runs on every regrid,
    ! so resizing down would thrash the device heap for nothing.  The device
    ! copy is torn down and recreated around the reallocation rather than
    ! updated afterwards - see the note on the declarations.
    nneed = max(n_lb_snd, n_lb_rcv)
    if (nneed > max_buff) then
      if (allocated(snd_buff_lb)) then
        
        deallocate(snd_buff_lb, rcv_buff_lb)
      end if
      max_buff = nneed
      allocate(snd_buff_lb(max_buff*nchunk), rcv_buff_lb(max_buff*nchunk))
      
    end if
    if (n_lb_snd == 0 .and. n_lb_rcv == 0) return

    if (n_lb_snd > 0) then
      
    end if
    if (n_lb_rcv > 0) then
      
    end if

    ! One Irecv per peer, straight into that peer's run. A peer sends exactly
    ! one message, so (communicator, source) already disambiguates and the tag
    ! carries nothing.
    itag = 0
    if (n_lb_recv_pe > 0) then
#ifndef NOGPUDIRECT
      
#endif
      do k = 1, n_lb_recv_pe
        call mpi_irecv_wrapper(rcv_buff_lb(lb_recv_pe_off(k)),&
           lb_recv_pe_len(k),MPI_DOUBLE_PRECISION,lb_recv_pe(k),itag,icomm,&
           recvrequest(k),ierrmpi)
      end do
#ifndef NOGPUDIRECT
      
#endif
    end if

    ! Pack every departing block in one kernel. bg(1)%w carries the grid index
    ! last, so the block can be selected by a device-side index.
    if (n_lb_snd > 0) then
       
      do k = 1, n_lb_snd
         igrid = lb_snd_igrid(k)
         ibuf  = lb_snd_ibuf(k)
         
         do iw = 1, nwgc
            do ix3 = 1, block_nx3
               do ix2 = 1, block_nx2
                  do ix1 = 1, block_nx1
                     snd_buff_lb(ibuf + (ix1-1) + block_nx1*(ix2-1) &
                          + block_nx1*block_nx2*(ix3-1) &
                          + block_nx1*block_nx2*block_nx3*(iw-1)) = &
                          bg(1)%w(ixMlo1-1+ix1, ixMlo2-1+ix2, ixMlo3-1+ix3,&
                                  iw, igrid)
                  end do
               end do
            end do
         end do
      end do
    end if

    if (n_lb_send_pe > 0) then
#ifdef NOGPUDIRECT
      
#else
      
#endif
      do k = 1, n_lb_send_pe
        call mpi_isend_wrapper(snd_buff_lb(lb_send_pe_off(k)),&
           lb_send_pe_len(k),MPI_DOUBLE_PRECISION,lb_send_pe(k),itag,icomm,&
           sendrequest(k),ierrmpi)
      end do
#ifndef NOGPUDIRECT
      
#endif
    end if

    if (n_lb_recv_pe > 0) then
      call MPI_WAITALL(n_lb_recv_pe,recvrequest,recvstatus,ierrmpi)
      ! With one aggregated message per peer a disagreement about which blocks
      ! the run holds is no longer an MPI mismatch, so check the arrival
      ! lengths. A difference in order cannot arise: both ranks sort the same
      ! run by the same key.
      do k = 1, n_lb_recv_pe
        call MPI_GET_COUNT(recvstatus(:,k),MPI_DOUBLE_PRECISION,nbuf,ierrmpi)
        if (nbuf /= lb_recv_pe_len(k)) call mpistop( &
           "exchange_migrated_blocks: message length disagrees with the layout")
      end do
#ifdef NOGPUDIRECT
      
#endif
    end if

    ! Apply every arriving block in one kernel.
    if (n_lb_rcv > 0) then
       
      do k = 1, n_lb_rcv
         igrid = lb_rcv_igrid(k)
         ibuf  = lb_rcv_ibuf(k)
         
         do iw = 1, nwgc
            do ix3 = 1, block_nx3
               do ix2 = 1, block_nx2
                  do ix1 = 1, block_nx1
                     bg(1)%w(ixMlo1-1+ix1, ixMlo2-1+ix2, ixMlo3-1+ix3,&
                             iw, igrid) &
                          = rcv_buff_lb(ibuf + (ix1-1) + block_nx1*(ix2-1) &
                            + block_nx1*block_nx2*(ix3-1) &
                            + block_nx1*block_nx2*block_nx3*(iw-1))
                  end do
               end do
            end do
         end do
      end do
    end if

    if (n_lb_send_pe > 0) then
      call MPI_WAITALL(n_lb_send_pe,sendrequest,sendstatus,ierrmpi)
    end if

  end subroutine exchange_migrated_blocks

  subroutine lb_recv
    use mod_global_parameters
    use mod_amr_solution_node, only: alloc_node
    use mod_comm_lib, only: mpistop

    ! alloc_node also re-derives the analytic extras (for ffhd, the frozen
    ! field) through fill_nwextra_device, which is what lets the transfer below
    ! carry only 1:nwgc rather than 1:nw.
    call alloc_node(recv_igrid)

    ! Record the block; exchange_migrated_blocks posts them all once the walk
    ! is over, one message per peer.  The key is the index this block will
    ! occupy here, which the sender knows too.
    irecv=irecv+1
    n_lb_rcv=n_lb_rcv+1
    if (n_lb_rcv > size(lb_rcv_igrid)) then
       call mpistop('load_balance: more arrivals than max_blocks')
    end if
    lb_rcv_igrid(n_lb_rcv) = recv_igrid
    lb_rcv_src(n_lb_rcv)   = send_ipe
    lb_rcv_key(n_lb_rcv)   = recv_igrid
    if(stagger_grid) then
       itag=recv_igrid+max_blocks
       call mpi_irecv_wrapper(ps(recv_igrid)%ws,1,type_block_io_stg,send_ipe,itag,&
             icomm,recvrequest_stg(irecv),ierrmpi)
    end if
  end subroutine lb_recv

  subroutine lb_send
    use mod_global_parameters
    use mod_comm_lib, only: mpistop

    ! Record the block; the pack and the post both happen in
    ! exchange_migrated_blocks.  The key is the receiving block index, so it
    ! matches the one the receiver derives from its own getnode().
    isend=isend+1
    n_lb_snd=n_lb_snd+1
    if (n_lb_snd > size(lb_snd_igrid)) then
       call mpistop('load_balance: more departures than max_blocks')
    end if
    lb_snd_igrid(n_lb_snd) = send_igrid
    lb_snd_dest(n_lb_snd)  = recv_ipe
    lb_snd_key(n_lb_snd)   = recv_igrid

    ! The staggered branch below is NOT aggregated and is left as found: it is
    ! unreachable (stagger_grid is false throughout this fork and fix_edges has
    ! no caller).  Its tag, recv_igrid+max_blocks, is sound but doubles the tag
    ! range, so it would be the first thing to hit MPI_TAG_UB.
    if(stagger_grid) then
       itag=recv_igrid+max_blocks
       call mpi_isend_wrapper(ps(send_igrid)%ws,1,type_block_io_stg,recv_ipe,itag,&
             icomm,sendrequest_stg(isend),ierrmpi)
    end if
  end subroutine lb_send

end module mod_load_balance
