module mod_functions_forest
#ifdef USE_MPIWRAPPERS
  use mod_mpi_wrapper
#else
#define mpi_file_read_wrapper MPI_FILE_READ
#endif
  implicit none
  private

  public :: write_forest
  public :: read_forest
  public :: coarsen_tree_leaf
  public :: refine_tree_leaf
  public :: change_ipe_tree_leaf
  public :: init_forest_root

  !> Depth-first leaf flags of the whole forest, one LOGICAL per tree node.
  !> Buffered in chunks of nleafbuf rather than written one node at a time: the
  !> byte stream on disk is identical either way, but a tree of a million nodes
  !> costs a million individual MPI_FILE_WRITEs otherwise.  read_forest consumes
  !> the same stream through a buffer of its own.
  integer, parameter :: nleafbuf = 16384


contains


  !> build root forest
  subroutine init_forest_root
    use mod_forest
    use mod_global_parameters
    use mod_space_filling_curve
    use mod_amr_solution_node, only: getnode
    use mod_amr_neighbors, only: find_root_neighbor

    integer :: ig1,ig2,ig3, level, igrid, ipe
    integer :: iside, i1,i2,i3, Morton_no, isfc


    level=1
    Morton_no=0
    ipe=0
    nparents=0
    nleafs=ng1(1)*ng2(1)*ng3(1)
    nleafs_active=nleafs
    nleafs_level(1)=ng1(1)*ng2(1)*ng3(1)
    nleafs_level(2:nlevelshi)=0
    call get_Morton_range
    ! Generate Morton-order space-filling curve to connect level 1 blocks
    call level1_Morton_order
    do isfc=1,nglev1
       ig1=sfc_iglevel1(1,isfc)
       ig2=sfc_iglevel1(2,isfc)
       ig3=sfc_iglevel1(3,isfc)
       Morton_no=Morton_no+1
       if (Morton_no>Morton_stop(ipe)) ipe=ipe+1
       igrid=getnode(ipe)
       if (ipe==mype) then
          sfc_to_igrid(Morton_no)=igrid
          igrid_to_sfc(igrid)=Morton_no
       end if
       call init_tree_leaf(tree_root(ig1,ig2,ig3),ig1,ig2,ig3,level,igrid,ipe,&
          .true.)
    end do

    ! update root neighbor
    do ig3=1,ng3(1)
    do ig2=1,ng2(1)
    do ig1=1,ng1(1)
       do iside=1,2
          i1=kr(1,1)*(2*iside-3);i2=kr(2,1)*(2*iside-3)
          i3=kr(3,1)*(2*iside-3);
          call find_root_neighbor(tree_root(ig1,ig2,ig3)%node%neighbor(iside,&
             1), tree_root(ig1,ig2,ig3),i1,i2,i3)
       end do
       do iside=1,2
          i1=kr(1,2)*(2*iside-3);i2=kr(2,2)*(2*iside-3)
          i3=kr(3,2)*(2*iside-3);
          call find_root_neighbor(tree_root(ig1,ig2,ig3)%node%neighbor(iside,&
             2), tree_root(ig1,ig2,ig3),i1,i2,i3)
       end do
       do iside=1,2
          i1=kr(1,3)*(2*iside-3);i2=kr(2,3)*(2*iside-3)
          i3=kr(3,3)*(2*iside-3);
          call find_root_neighbor(tree_root(ig1,ig2,ig3)%node%neighbor(iside,&
             3), tree_root(ig1,ig2,ig3),i1,i2,i3)
       end do
    end do
    end do
    end do

    ! This call is here to ensure the sfc array is initialized
    call amr_Morton_order()

  end subroutine init_forest_root

  subroutine init_tree_leaf(tree,ig1,ig2,ig3,level,igrid,ipe,active)
    use mod_forest
    implicit none

    type(tree_node_ptr) :: tree
    integer, intent(in) :: ig1,ig2,ig3, level, igrid, ipe
    logical, intent(in) :: active
    integer :: ic1,ic2,ic3

    allocate(tree%node)

    tree%node%ig1=ig1;tree%node%ig2=ig2;tree%node%ig3=ig3;
    tree%node%level=level
    tree%node%igrid=igrid
    tree%node%ipe=ipe

    tree%node%leaf=.true.
    tree%node%active=active

    nullify(tree%node%parent%node)
    do ic3=1,2
    do ic2=1,2
    do ic1=1,2
       nullify(tree%node%child(ic1,ic2,ic3)%node)
    end do
    end do
    end do

    call add_to_linked_list(level,tree)

    ! initialize neighbor pointers
    nullify(tree%node%neighbor(1,1)%node,tree%node%neighbor(1,2)%node,&
       tree%node%neighbor(1,3)%node,tree%node%neighbor(2,1)%node,&
       tree%node%neighbor(2,2)%node,tree%node%neighbor(2,3)%node)

    igrid_to_node(igrid,ipe)%node => tree%node

  end subroutine init_tree_leaf

  subroutine coarsen_tree_leaf(igrid,ipe,child_igrid,child_ipe,active)
    use mod_forest
    implicit none

    integer, intent(in) :: igrid, ipe
    integer, dimension(2,2,2), intent(in) :: child_igrid, child_ipe
    logical, intent(out) :: active


    integer :: level, ic1,ic2,ic3, child_level, iside, iotherside, vote
    type(tree_node_ptr) :: tree, child, child_neighbor

    tree%node => igrid_to_node(child_igrid(1,1,1),child_ipe(1,1,&
       1))%node%parent%node
    level=tree%node%level

    call add_to_linked_list(level,tree)

    child_level=level+1
    vote=0

    do ic3=1,2
    do ic2=1,2
    do ic1=1,2
       child%node => tree%node%child(ic1,ic2,ic3)%node

    !  vote for active:
       if(child%node%active) vote=vote+1

       call delete_from_linked_list(child_level,child)

       ! update neighbor pointers
       iside=ic1
       child_neighbor%node => child%node%neighbor(iside,1)%node
       if (associated(child_neighbor%node)) then
          if (child%node%ig1==child_neighbor%node%ig1) then ! pole
             nullify(child_neighbor%node%neighbor(iside,1)%node)
          else
             iotherside=3-iside
             nullify(child_neighbor%node%neighbor(iotherside,1)%node)
          end if
       end if
       iside=ic2
       child_neighbor%node => child%node%neighbor(iside,2)%node
       if (associated(child_neighbor%node)) then
          if (child%node%ig2==child_neighbor%node%ig2) then ! pole
             nullify(child_neighbor%node%neighbor(iside,2)%node)
          else
             iotherside=3-iside
             nullify(child_neighbor%node%neighbor(iotherside,2)%node)
          end if
       end if
       iside=ic3
       child_neighbor%node => child%node%neighbor(iside,3)%node
       if (associated(child_neighbor%node)) then
          if (child%node%ig3==child_neighbor%node%ig3) then ! pole
             nullify(child_neighbor%node%neighbor(iside,3)%node)
          else
             iotherside=3-iside
             nullify(child_neighbor%node%neighbor(iotherside,3)%node)
          end if
       end if

       nullify(tree%node%child(ic1,ic2,ic3)%node)
       deallocate(igrid_to_node(child_igrid(ic1,ic2,ic3),child_ipe(ic1,ic2,&
          ic3))%node)
    end do
    end do
    end do

    tree%node%leaf=.true.
    tree%node%igrid=igrid
    tree%node%ipe=ipe
    igrid_to_node(igrid,ipe)%node => tree%node

    !  Count the vote and set active/passive state:

    if (vote /= 2**3) then
    !if (vote == 0) then
       tree%node%active = .false.
       nleafs_active = nleafs_active - vote
    else
       tree%node%active = .true.
       nleafs_active = nleafs_active - vote + 1
    end if
    active = tree%node%active

    nleafs=nleafs-2**3+1
    nparents=nparents-1
    nleafs_level(child_level)=nleafs_level(child_level)-2**3
    nleafs_level(level)=nleafs_level(level)+1

  end subroutine coarsen_tree_leaf

  subroutine refine_tree_leaf(child_igrid,child_ipe,igrid,ipe,active)
    use mod_forest
    use mod_global_parameters
    use mod_amr_neighbors, only: find_neighbor

    integer, dimension(2,2,2), intent(in) :: child_igrid, child_ipe
    integer, intent(in) :: igrid, ipe
    logical, intent(out):: active

    integer :: ig1,ig2,ig3, level, i1,i2,i3, ic1,ic2,ic3, child_ig1,child_ig2,&
       child_ig3, child_level, iside
    integer :: my_neighbor_type
    logical, dimension(ndim) :: pole
    type(tree_node_ptr) :: tree, child, my_neighbor

    tree%node => igrid_to_node(igrid,ipe)%node
    ig1=tree%node%ig1;ig2=tree%node%ig2;ig3=tree%node%ig3;
    level=tree%node%level
    active=tree%node%active

    tree%node%ipe=-1
    tree%node%igrid=0
    tree%node%leaf=.false.
    tree%node%active=.true.

    call delete_from_linked_list(level,tree)

    child_level=level+1

    do ic3=1,2
    do ic2=1,2
    do ic1=1,2
       child_ig1=2*(ig1-1)+ic1;child_ig2=2*(ig2-1)+ic2
       child_ig3=2*(ig3-1)+ic3;
       call init_tree_leaf(child,child_ig1,child_ig2,child_ig3,child_level,&
           child_igrid(ic1,ic2,ic3),child_ipe(ic1,ic2,ic3),active)

       igrid_to_node(child_igrid(ic1,ic2,ic3),child_ipe(ic1,ic2,&
          ic3))%node => child%node

       tree%node%child(ic1,ic2,ic3)%node => child%node
       child%node%parent%node => tree%node
    end do
    end do
    end do

    ! update neighbor pointers
    do ic3=1,2
    do ic2=1,2
    do ic1=1,2
       child%node => tree%node%child(ic1,ic2,ic3)%node
       iside=ic1
       i1=kr(1,1)*(2*iside-3);i2=kr(2,1)*(2*iside-3);i3=kr(3,1)*(2*iside-3);
       call find_neighbor(my_neighbor,my_neighbor_type,child,i1,i2,i3,pole)
       select case (my_neighbor_type)
       case (neighbor_sibling, neighbor_fine)
          child%node%neighbor(iside,1)%node => my_neighbor%node
          if (pole(1)) then
             my_neighbor%node%neighbor(iside,1)%node => child%node
          else
             my_neighbor%node%neighbor(3-iside,1)%node => child%node
          end if
       case default
          nullify(child%node%neighbor(iside,1)%node)
       end select
       child%node%neighbor(3-ic1,1)%node=>tree%node%child(3-ic1,ic2,ic3)%node
       iside=ic2
       i1=kr(1,2)*(2*iside-3);i2=kr(2,2)*(2*iside-3);i3=kr(3,2)*(2*iside-3);
       call find_neighbor(my_neighbor,my_neighbor_type,child,i1,i2,i3,pole)
       select case (my_neighbor_type)
       case (neighbor_sibling, neighbor_fine)
          child%node%neighbor(iside,2)%node => my_neighbor%node
          if (pole(2)) then
             my_neighbor%node%neighbor(iside,2)%node => child%node
          else
             my_neighbor%node%neighbor(3-iside,2)%node => child%node
          end if
       case default
          nullify(child%node%neighbor(iside,2)%node)
       end select
       child%node%neighbor(3-ic2,2)%node=>tree%node%child(ic1,3-ic2,ic3)%node
       iside=ic3
       i1=kr(1,3)*(2*iside-3);i2=kr(2,3)*(2*iside-3);i3=kr(3,3)*(2*iside-3);
       call find_neighbor(my_neighbor,my_neighbor_type,child,i1,i2,i3,pole)
       select case (my_neighbor_type)
       case (neighbor_sibling, neighbor_fine)
          child%node%neighbor(iside,3)%node => my_neighbor%node
          if (pole(3)) then
             my_neighbor%node%neighbor(iside,3)%node => child%node
          else
             my_neighbor%node%neighbor(3-iside,3)%node => child%node
          end if
       case default
          nullify(child%node%neighbor(iside,3)%node)
       end select
       child%node%neighbor(3-ic3,3)%node=>tree%node%child(ic1,ic2,3-ic3)%node
    end do
    end do
    end do

    nleafs=nleafs+2**3-1
    nparents=nparents+1
    nleafs_level(child_level)=nleafs_level(child_level)+2**3
    nleafs_level(level)=nleafs_level(level)-1

    if (active) nleafs_active = nleafs_active + 2**3-1

  end subroutine refine_tree_leaf

  subroutine change_ipe_tree_leaf(recv_igrid,recv_ipe,send_igrid,send_ipe)
    use mod_forest
    implicit none

    integer, intent(in) :: recv_igrid, recv_ipe, send_igrid, send_ipe

    type(tree_node_ptr) :: tree

    tree%node => igrid_to_node(send_igrid,send_ipe)%node

    tree%node%igrid=recv_igrid
    tree%node%ipe=recv_ipe

    nullify(igrid_to_node(send_igrid,send_ipe)%node)
    igrid_to_node(recv_igrid,recv_ipe)%node => tree%node

  end subroutine change_ipe_tree_leaf

  subroutine add_to_linked_list(level,tree)
    use mod_forest
    implicit none

    integer, intent(in) :: level
    type(tree_node_ptr) :: tree

    nullify(tree%node%next%node)
    if (associated(level_head(level)%node)) then
       tree%node%prev%node => level_tail(level)%node
       level_tail(level)%node%next%node => tree%node
       level_tail(level)%node => tree%node
    else
       level_head(level)%node => tree%node
       level_tail(level)%node => tree%node
       nullify(tree%node%prev%node)
    end if

  end subroutine add_to_linked_list

  subroutine delete_from_linked_list(level,tree)
    use mod_forest
    implicit none

    integer, intent(in) :: level
    type(tree_node_ptr) :: tree

    type(tree_node_ptr) :: next, prev

    prev%node => tree%node%prev%node
    next%node => tree%node%next%node
    if (associated(next%node).and.associated(prev%node)) then
       prev%node%next%node => next%node
       next%node%prev%node => prev%node
    else if (associated(prev%node)) then
       level_tail(level)%node => prev%node
       nullify(prev%node%next%node)
    else if (associated(next%node)) then
       level_head(level)%node => next%node
       nullify(next%node%prev%node)
    else
       nullify(level_head(level)%node)
       nullify(level_tail(level)%node)
    end if

  end subroutine delete_from_linked_list

  subroutine write_forest(file_handle)
    use mod_forest
    use mod_global_parameters

    integer, intent(in) :: file_handle

    integer, dimension(MPI_STATUS_SIZE) :: status
    integer :: ig1,ig2,ig3,isfc
    logical :: leafbuf(nleafbuf)
    integer :: nbuffered

    nbuffered = 0
    do isfc=1,nglev1
       ig1=sfc_iglevel1(1,isfc)
       ig2=sfc_iglevel1(2,isfc)
       ig3=sfc_iglevel1(3,isfc)
       call write_node(tree_root(ig1,ig2,ig3))
    end do
    call flush_leafbuf

    contains

      recursive subroutine write_node(tree)
        implicit none

        type(tree_node_ptr) :: tree

        integer :: ic1,ic2,ic3

        if (nbuffered == nleafbuf) call flush_leafbuf
        nbuffered = nbuffered + 1
        leafbuf(nbuffered) = tree%node%leaf

        if (.not.tree%node%leaf) then
           do ic3=1,2
           do ic2=1,2
           do ic1=1,2
              call write_node(tree%node%child(ic1,ic2,ic3))
           end do
           end do
           end do
        end if

      end subroutine write_node

      subroutine flush_leafbuf
        implicit none

        if (nbuffered == 0) return
        call MPI_FILE_WRITE(file_handle,leafbuf,nbuffered,MPI_LOGICAL,status,&
           ierrmpi)
        nbuffered = 0

      end subroutine flush_leafbuf

  end subroutine write_forest

  subroutine read_forest(file_handle)
    use mod_forest
    use mod_global_parameters
    use mod_space_filling_curve
    use mod_amr_solution_node, only: getnode
    use mod_functions_connectivity, only: get_level_range,build_connectivity,&
       getigrids
    use mod_amr_neighbors, only: asign_tree_neighbor

    integer, intent(in) :: file_handle

    integer, dimension(MPI_STATUS_SIZE) :: status
    !integer :: ig^D, level, size_logical, Morton_no, igrid, ipe
    integer :: ig1,ig2,ig3, level, Morton_no, igrid, ipe, isfc
    logical :: leafbuf(nleafbuf)
    integer :: ibuf, nvalid, nread

    Morton_no=0
    ipe=0
    level=1
    nleafs_level(1:nlevelshi) = 0
    nparents = 0
    ! force a fill on the first node
    ibuf   = 0
    nvalid = 0
    nread  = 0

    call get_Morton_range
    call level1_Morton_order
    do isfc=1,nglev1
       ig1=sfc_iglevel1(1,isfc)
       ig2=sfc_iglevel1(2,isfc)
       ig3=sfc_iglevel1(3,isfc)
       allocate(tree_root(ig1,ig2,ig3)%node)
       nullify(tree_root(ig1,ig2,ig3)%node%parent%node)
       call read_node(tree_root(ig1,ig2,ig3),ig1,ig2,ig3,level)
    end do

    ! Hand back the read-ahead: leave rank 0's file pointer exactly where the
    ! last consumed node ends, which is where it sat when this routine read one
    ! node at a time.  read_snapshot_old relies on the sequential position.
    if (mype==0 .and. nread>ibuf) then
       call MPI_FILE_SEEK(file_handle, -int(nread-ibuf,kind=MPI_OFFSET_KIND)* &
          int(size_logical,kind=MPI_OFFSET_KIND), MPI_SEEK_CUR, ierrmpi)
    end if

    call get_level_range

    ! Rebuild tree connectivity
    call getigrids
    call build_connectivity

    ! This call is here to ensure the sfc array is initialized
    call amr_Morton_order()

    contains

      recursive subroutine read_node(tree,ig1,ig2,ig3,level)
        implicit none

        type(tree_node_ptr) :: tree
        integer, intent(in) :: ig1,ig2,ig3, level

        logical :: leaf
        integer :: ic1,ic2,ic3, child_ig1,child_ig2,child_ig3, child_level

        if (ibuf == nvalid) call refill_leafbuf
        ibuf = ibuf + 1
        leaf = leafbuf(ibuf)

        tree%node%leaf=leaf
        tree%node%ig1=ig1;tree%node%ig2=ig2;tree%node%ig3=ig3;
        tree%node%level=level
        tree%node%active=.true. .and. leaf

        do ic3=1,2
        do ic2=1,2
        do ic1=1,2
           nullify(tree%node%child(ic1,ic2,ic3)%node)
        end do
        end do
        end do
        nullify(tree%node%neighbor(1,1)%node,tree%node%neighbor(1,2)%node,&
           tree%node%neighbor(1,3)%node,tree%node%neighbor(2,1)%node,&
           tree%node%neighbor(2,2)%node,tree%node%neighbor(2,3)%node)
        nullify(tree%node%next%node,tree%node%prev%node)

        call asign_tree_neighbor(tree)

        if (leaf) then
           call add_to_linked_list(level,tree)
           nleafs_level(level) = nleafs_level(level) + 1

           Morton_no=Morton_no+1
           if (Morton_no>Morton_stop(ipe)) ipe=ipe+1
           igrid=getnode(ipe)
           tree%node%igrid=igrid
           tree%node%ipe=ipe
           igrid_to_node(igrid,ipe)%node => tree%node
           if (ipe==mype) sfc_to_igrid(Morton_no)=igrid
         else
           nparents = nparents + 1
           tree%node%igrid=0
           tree%node%ipe=-1
           child_level=level+1
           do ic3=1,2
           do ic2=1,2
           do ic1=1,2
              child_ig1=2*(ig1-1)+ic1;child_ig2=2*(ig2-1)+ic2
              child_ig3=2*(ig3-1)+ic3;
              allocate(tree%node%child(ic1,ic2,ic3)%node)
              tree%node%child(ic1,ic2,ic3)%node%parent%node => tree%node
              call read_node(tree%node%child(ic1,ic2,ic3),child_ig1,child_ig2,&
                 child_ig3,child_level)
           end do
           end do
           end do
        end if

      end subroutine read_node

      !> Pull the next chunk of leaf flags in, one read and one broadcast for
      !> up to nleafbuf nodes instead of one of each per node.  Every rank walks
      !> the tree in lockstep - the recursion is driven only by flags they all
      !> hold - so they run out of buffer at the same node and this stays
      !> collective without any agreement beyond that.
      subroutine refill_leafbuf
        implicit none

        if (mype==0) then
          call mpi_file_read_wrapper(file_handle,leafbuf,nleafbuf,MPI_LOGICAL,&
             status,ierrmpi)
          call MPI_GET_COUNT(status,MPI_LOGICAL,nread,ierrmpi)
        end if
        if (npe>1) call MPI_BCAST(leafbuf,nleafbuf,MPI_LOGICAL,0,icomm,ierrmpi)

        ! A short read at end of file is fine: the walk stops when the tree is
        ! complete and never looks past the last node the file holds, so the
        ! tail of the buffer is not consumed.  nvalid therefore counts the whole
        ! buffer, and only rank 0 tracks what it actually read, for the rewind.
        ibuf   = 0
        nvalid = nleafbuf

      end subroutine refill_leafbuf

  end subroutine read_forest


end module mod_functions_forest
