!! Constant-coefficient horizontal tracer diffusion for the ocean
!! multilayer C-grid.  Conservative curvilinear flux-form Laplacian
!! on `T = hTr/h` (design §2, mirrors continuity):
!!
!!   F_x(i,j,k) = kappa * h_face_x * (T(i,j,k) - T(i-1,j,k)) * idxCu * dy_cu
!!   hTr(i,j,k) += dt * [(F_x(i+1)-F_x(i)) + (F_y(j+1)-F_y(j))] * iareaT
!!
!! The face flux carries the width-weighted transport [tracer·m³/s]
!! and the divergence is area-normalised by `iareaT`; on uniform
!! Cartesian this collapses to the `/dx`/`/dy` form to round-off.
!! `h_face_x = 0.5 * (h(i-1, j, k) + h(i, j, k))` keeps the kernel
!! conservative under variable thickness — the divergence of an
!! h-weighted flux integrates to zero over the closed-wall domain.
!!
!! Wall faces force `F = 0` (closed-wall, no tracer flux across the
!! boundary).  Vanishing-layer guards on the `T = hTr/h` division
!! mirror the horizontal advection kernel.
!!
!! Phase Tier-1 ships the constant-`kappa_h` variant; Phase 5+ may
!! drop in a Smagorinsky-style isotropic closure or a Redi-style
!! isopycnal rotation.
module rdb_ocean_hdiff_tracer
   !! Kernel state for the per-tracer horizontal-diffusion sweep.
   !! Sits alongside `rdb_ocean_horizontal_viscosity` (which does
   !! the momentum half of horizontal mixing); this module is the
   !! tracer-side counterpart.  Same compute-then-apply pattern,
   !! same scratch-buffer two-pass design for race-free
   !! `do concurrent` parallelism.
   use rdb_constants, only: wp
   use rdb_grid, only: hgrid_t
   use rdb_ocean_metrics, only: ocean_metrics_t
   use rdb_multilayer_state, only: multilayer_state_t
   use rdb_ocean_boundary_types, only: ocean_bc_state_t, OBC_WALL, ocean_bc_outer_face_tag
   use rdb_tracer, only: TRACER_BUDGET_HEAT, TRACER_BUDGET_SALT
   use rdb_scratch_3d, only: scratch_3d_buffer_t, &
                             scratch_3d_buffer_enter_data_impl, &
                             scratch_3d_buffer_exit_data_impl
   use, intrinsic :: iso_fortran_env, only: int64
   use rdb_mem_report, only: arr_bytes
   implicit none
   private

   public :: ocean_hdiff_tracer_t
   public :: tracer_hdiff

   type :: ocean_hdiff_tracer_t
      logical :: is_init = .false.
         !! True between `init` and `destroy`.
      real(wp) :: kappa_h = 0.0_wp
         !! Constant horizontal tracer diffusivity (m^2/s).  Zero
         !! is a no-op — the kernel short-circuits without
         !! touching hTr.  Stability bound (explicit forward-Euler):
         !!   kappa_h * dt * (1/dx^2 + 1/dy^2) <= 0.5

      type(scratch_3d_buffer_t) :: T_centre
         !! `T = hTr/h` at cell centres.  Shape (nx, ny, nz_ml).
         !! Filled per tracer at the start of each impl call;
         !! reused across the F_x / F_y face passes so the
         !! divergence step reads a consistent snapshot.
      type(scratch_3d_buffer_t) :: F_x_face
         !! East-face tracer flux `kappa * h_face * dT/dx`.
         !! Shape (nx+1, ny, nz_ml).
      type(scratch_3d_buffer_t) :: F_y_face
         !! North-face counterpart.  Shape (nx, ny+1, nz_ml).
   contains
      procedure, non_overridable :: init => ocean_hdiff_tracer_init
      procedure, non_overridable :: destroy => ocean_hdiff_tracer_destroy
      procedure, non_overridable :: enter_data => ocean_hdiff_tracer_enter_data
      procedure, non_overridable :: exit_data => ocean_hdiff_tracer_exit_data
      procedure, non_overridable :: bytes => ocean_hdiff_tracer_bytes
   end type ocean_hdiff_tracer_t

contains

   subroutine ocean_hdiff_tracer_init(this, grid, nz_ml)
      class(ocean_hdiff_tracer_t), intent(inout) :: this
      type(hgrid_t), intent(in) :: grid
      integer, intent(in), optional :: nz_ml
      integer :: nx, ny, nz

      nx = grid%nx_total
      ny = grid%ny_total
      nz = 1
      if (present(nz_ml)) nz = nz_ml

      call this%T_centre%init(nx, ny, nz, "ocean_hdiff_T_centre")
      call this%F_x_face%init(nx + 1, ny, nz, "ocean_hdiff_F_x_face")
      call this%F_y_face%init(nx, ny + 1, nz, "ocean_hdiff_F_y_face")
      this%is_init = .true.
   end subroutine ocean_hdiff_tracer_init

   subroutine ocean_hdiff_tracer_destroy(this)
      class(ocean_hdiff_tracer_t), intent(inout) :: this
      this%is_init = .false.
      call this%T_centre%destroy()
      call this%F_x_face%destroy()
      call this%F_y_face%destroy()
   end subroutine ocean_hdiff_tracer_destroy

   subroutine ocean_hdiff_tracer_enter_data(this)
      class(ocean_hdiff_tracer_t), intent(inout) :: this
      select type (this)
      type is (ocean_hdiff_tracer_t)
         call ocean_hdiff_tracer_enter_data_impl(this)
      end select
   end subroutine ocean_hdiff_tracer_enter_data

   subroutine ocean_hdiff_tracer_enter_data_impl(this)
      type(ocean_hdiff_tracer_t), intent(inout) :: this
      call scratch_3d_buffer_enter_data_impl(this%T_centre)
      call scratch_3d_buffer_enter_data_impl(this%F_x_face)
      call scratch_3d_buffer_enter_data_impl(this%F_y_face)
   end subroutine ocean_hdiff_tracer_enter_data_impl

   subroutine ocean_hdiff_tracer_exit_data(this)
      class(ocean_hdiff_tracer_t), intent(inout) :: this
      select type (this)
      type is (ocean_hdiff_tracer_t)
         call ocean_hdiff_tracer_exit_data_impl(this)
      end select
   end subroutine ocean_hdiff_tracer_exit_data

   subroutine ocean_hdiff_tracer_exit_data_impl(this)
      type(ocean_hdiff_tracer_t), intent(inout) :: this
      call scratch_3d_buffer_exit_data_impl(this%T_centre)
      call scratch_3d_buffer_exit_data_impl(this%F_x_face)
      call scratch_3d_buffer_exit_data_impl(this%F_y_face)
   end subroutine ocean_hdiff_tracer_exit_data_impl

   subroutine tracer_hdiff(grid, metrics, this, ms, dt, active, bc)
      !! Iterate the tracer registry and apply constant-`kappa_h`
      !! horizontal Laplacian diffusion to every tracer whose
      !! `do_horizontal_diffusion` flag is set.  Outer-shim:
      !! forwards each tracer's hTr to the flat-impl below.
      !! Reuses the same scratch buffers across all tracers in one
      !! call.
      !!
      !! Optional `active` — when present and false the kernel is a
      !! no-op (used by the dyn step to gate on
      !! thermodynamics + thermo-substep cadence).
      !!
      !! Optional `bc` — per-edge OBC tags, resolved to plain host
      !! logicals before the kernel call (mirrors
      !! `continuity_zonal_flux`; a derived-type dummy must never
      !! reach a `do concurrent` kernel — mem:separate).  Absent =>
      !! WALL on every edge (single-rank default).  A non-WALL edge,
      !! or an MPI seam (`has_* = .false.`), leaves the physical-edge
      !! flux computed rather than hard-zeroed — the halo/periodic-wrap
      !! preamble the split-solver caller runs immediately before this
      !! call has already filled the ghost band with the correct
      !! neighbour/periodic value there.
      type(hgrid_t), intent(in) :: grid
      type(ocean_metrics_t), intent(in) :: metrics
      type(ocean_hdiff_tracer_t), intent(inout) :: this
      type(multilayer_state_t), intent(inout) :: ms
      real(wp), intent(in) :: dt
      logical, intent(in), optional :: active
      type(ocean_bc_state_t), intent(in), optional :: bc

      integer :: it
      logical :: wall_w, wall_e, wall_s, wall_n

      if (present(active)) then
         if (.not. active) return
      end if
      if (.not. allocated(ms%tracers)) return
      if (this%kappa_h <= 0.0_wp) return

      ! Resolve the physical-edge wall flags on the host — see
      ! `continuity_zonal_flux` for the identical pattern.  Default
      ! .true. => single-rank, all-wall closed boundary (bit-identical
      ! to a run with no `bc`).
      wall_w = .true.
      wall_e = .true.
      wall_s = .true.
      wall_n = .true.
      if (present(bc)) then
         wall_w = (ocean_bc_outer_face_tag(bc%west%bc_type) == OBC_WALL) .and. bc%has_west
         wall_e = (ocean_bc_outer_face_tag(bc%east%bc_type) == OBC_WALL) .and. bc%has_east
         wall_s = (ocean_bc_outer_face_tag(bc%south%bc_type) == OBC_WALL) .and. bc%has_south
         wall_n = (ocean_bc_outer_face_tag(bc%north%bc_type) == OBC_WALL) .and. bc%has_north
      end if

      ! The mask actuals are chosen ONCE, outside the tracer loop: they are
      ! passed as ABSENT optionals on the default path (see the `open_u`
      ! docstring for why an inert stand-in is not available here), and
      ! an absent optional cannot be selected inside an expression, so
      ! the select-case is written twice rather than the arguments once.
      ! Thermo cadence, cold code.
      if (metrics%use_closed_faces) then
         do it = 1, size(ms%tracers)
            if (.not. ms%tracers(it)%do_horizontal_diffusion) cycle
            select case (ms%tracers(it)%budget_id)
            case (TRACER_BUDGET_HEAT)
               call tracer_hdiff_one_impl( &
                  grid%nx_total, grid%ny_total, ms%nz_ml, &
                  grid%nghost, grid%nx_phys, grid%ny_phys, &
                  dt, this%kappa_h, &
                  metrics%dy_cu, metrics%dx_cv, metrics%idxCu, metrics%idyCv, &
                  metrics%iareaT, &
                  ms%h_layer, ms%tracers(it)%hTr, &
                  this%T_centre%data, &
                  this%F_x_face%data, this%F_y_face%data, &
                  wall_w, wall_e, wall_s, wall_n, &
                  open_u=metrics%open_u, open_v=metrics%open_v, &
                  budget=ms%heat_budget_hdiff)
            case (TRACER_BUDGET_SALT)
               call tracer_hdiff_one_impl( &
                  grid%nx_total, grid%ny_total, ms%nz_ml, &
                  grid%nghost, grid%nx_phys, grid%ny_phys, &
                  dt, this%kappa_h, &
                  metrics%dy_cu, metrics%dx_cv, metrics%idxCu, metrics%idyCv, &
                  metrics%iareaT, &
                  ms%h_layer, ms%tracers(it)%hTr, &
                  this%T_centre%data, &
                  this%F_x_face%data, this%F_y_face%data, &
                  wall_w, wall_e, wall_s, wall_n, &
                  open_u=metrics%open_u, open_v=metrics%open_v, &
                  budget=ms%salt_budget_hdiff)
            case default
               call tracer_hdiff_one_impl( &
                  grid%nx_total, grid%ny_total, ms%nz_ml, &
                  grid%nghost, grid%nx_phys, grid%ny_phys, &
                  dt, this%kappa_h, &
                  metrics%dy_cu, metrics%dx_cv, metrics%idxCu, metrics%idyCv, &
                  metrics%iareaT, &
                  ms%h_layer, ms%tracers(it)%hTr, &
                  this%T_centre%data, &
                  this%F_x_face%data, this%F_y_face%data, &
                  wall_w, wall_e, wall_s, wall_n, &
                  open_u=metrics%open_u, open_v=metrics%open_v)
            end select
         end do
      else
         do it = 1, size(ms%tracers)
            if (.not. ms%tracers(it)%do_horizontal_diffusion) cycle
            select case (ms%tracers(it)%budget_id)
            case (TRACER_BUDGET_HEAT)
               call tracer_hdiff_one_impl( &
                  grid%nx_total, grid%ny_total, ms%nz_ml, &
                  grid%nghost, grid%nx_phys, grid%ny_phys, &
                  dt, this%kappa_h, &
                  metrics%dy_cu, metrics%dx_cv, metrics%idxCu, metrics%idyCv, &
                  metrics%iareaT, &
                  ms%h_layer, ms%tracers(it)%hTr, &
                  this%T_centre%data, &
                  this%F_x_face%data, this%F_y_face%data, &
                  wall_w, wall_e, wall_s, wall_n, &
                  budget=ms%heat_budget_hdiff)
            case (TRACER_BUDGET_SALT)
               call tracer_hdiff_one_impl( &
                  grid%nx_total, grid%ny_total, ms%nz_ml, &
                  grid%nghost, grid%nx_phys, grid%ny_phys, &
                  dt, this%kappa_h, &
                  metrics%dy_cu, metrics%dx_cv, metrics%idxCu, metrics%idyCv, &
                  metrics%iareaT, &
                  ms%h_layer, ms%tracers(it)%hTr, &
                  this%T_centre%data, &
                  this%F_x_face%data, this%F_y_face%data, &
                  wall_w, wall_e, wall_s, wall_n, &
                  budget=ms%salt_budget_hdiff)
            case default
               call tracer_hdiff_one_impl( &
                  grid%nx_total, grid%ny_total, ms%nz_ml, &
                  grid%nghost, grid%nx_phys, grid%ny_phys, &
                  dt, this%kappa_h, &
                  metrics%dy_cu, metrics%dx_cv, metrics%idxCu, metrics%idyCv, &
                  metrics%iareaT, &
                  ms%h_layer, ms%tracers(it)%hTr, &
                  this%T_centre%data, &
                  this%F_x_face%data, this%F_y_face%data, &
                  wall_w, wall_e, wall_s, wall_n)
            end select
         end do
      end if
   end subroutine tracer_hdiff

   pure subroutine tracer_hdiff_one_impl(nx, ny, nz, nghost, nx_phys, ny_phys, dt, kappa, &
                                         dy_cu, dx_cv, idxCu, idyCv, iareaT, &
                                         h, hTr, T_centre, F_x_face, F_y_face, &
                                         wall_w, wall_e, wall_s, wall_n, &
                                         open_u, open_v, budget)
      !! Four-pass flat-impl horizontal-Laplacian tracer diffusion in
      !! conservative curvilinear form (design §2, mirrors continuity):
      !!
      !!   1. Compute T = hTr / h at every cell centre (vanishing
      !!      layer → T = 0).
      !!   2. East-face TRANSPORT flux [tracer·m³/s]:
      !!        F_x(i) = kappa · 0.5·(h(i-1)+h(i)) · (T(i)-T(i-1))
      !!                 · idxCu(i) · dy_cu(i)
      !!      PHYSICAL wall faces (i = nghost+1, i = nghost+nx_phys+1):
      !!      zeroed when `wall_w`/`wall_e` (mirrors
      !!      `continuity_zonal_flux`'s host-resolved has_*/OBC_WALL
      !!      gate).  ARRAY-bound faces (i = 1, i = nx+1) are always
      !!      zeroed too — the pass-4 divergence reads F_x_face(1,...)
      !!      and F_x_face(nx+1,...) at the domain edge cells even
      !!      when the physical wall sits inside the ghost band, so
      !!      those two faces must stay initialised.
      !!   3. North-face flux, mirror of pass 2 (idyCv · dx_cv).
      !!   4. Divergence into hTr:
      !!        hTr(i,j,k) += dt · [(F_x(i+1)-F_x(i))
      !!                            + (F_y(j+1)-F_y(j))] · iareaT(i,j)
      !!
      !! On uniform Cartesian `dy_cu=dy`, `idxCu=1/dx`,
      !! `iareaT=1/(dx·dy)`, so the form collapses to the old
      !! `Δ(kappa·h·ΔT/dx)/dx` to round-off.  Each pass writes a
      !! different buffer than it reads, so `do concurrent` is
      !! race-free.  Constancy: uniform T → zero face fluxes → hTr
      !! unchanged.  Conservation over the PHYSICAL domain: closed
      !! physical-wall + flux-form divergence over the area-weighted
      !! cells integrates to zero exactly (see `tracer_hdiff`'s
      !! host-side wall-flag resolution for the OBC/MPI-seam gate).
      !!
      !! Stability (per cell, explicit forward-Euler):
      !!   kappa · dt · (idxT² + idyT²) <= 0.5.
      integer, intent(in) :: nx, ny, nz, nghost, nx_phys, ny_phys
      real(wp), intent(in) :: dt, kappa
      real(wp), intent(in) :: dy_cu(nx + 1, ny), dx_cv(nx, ny + 1)
      real(wp), intent(in) :: idxCu(nx + 1, ny), idyCv(nx, ny + 1)
      real(wp), intent(in) :: iareaT(nx, ny)
      real(wp), intent(in) :: h(nx, ny, nz)
      real(wp), intent(inout) :: hTr(nx, ny, nz)
      real(wp), intent(inout) :: T_centre(nx, ny, nz)
      real(wp), intent(inout) :: F_x_face(nx + 1, ny, nz)
      real(wp), intent(inout) :: F_y_face(nx, ny + 1, nz)
      logical, intent(in) :: wall_w, wall_e, wall_s, wall_n
      real(wp), intent(in), optional :: open_u(nx + 1, ny, nz)
         !! Per-layer 0/1 u-face open mask
         !! (`&vcoord_nml zfixed_closed_faces`).  A CLOSED face is a
         !! z-level WALL for that layer, so it carries no diffusive
         !! tracer flux either — the same statement continuity makes
         !! about mass.
         !!
         !! ABSENT (the default path) ⇒ no masking pass is generated and
         !! every expression below is byte-identical to the un-masked
         !! form.  It is OPTIONAL rather than a `use_open` + inert
         !! stand-in pair precisely because the `(1,1,1)` placeholder
         !! must never reach an explicit-shape dummy, and this routine
         !! has no full-size read-only array of its own to lend (the
         !! `F_*_face` buffers it would otherwise borrow are its own
         !! `intent(inout)` scratch, so lending them would alias).
      real(wp), intent(in), optional :: open_v(nx, ny + 1, nz)
         !! v-face twin.  Present iff `open_u` is.
      real(wp), intent(inout), optional :: budget(nx, ny, nz)

      integer :: i, j, k
      real(wp) :: h_face, delta

      ! ---- Pass 1: T = hTr/h at centres ----
      do concurrent(k=1:nz, j=1:ny, i=1:nx)
         if (h(i, j, k) > 0.0_wp) then
            T_centre(i, j, k) = hTr(i, j, k)/h(i, j, k)
         else
            T_centre(i, j, k) = 0.0_wp
         end if
      end do

      ! ---- Pass 2: east-face transport flux ----
      do concurrent(k=1:nz, j=1:ny, i=2:nx) local(h_face)
         h_face = 0.5_wp*(h(i - 1, j, k) + h(i, j, k))
         F_x_face(i, j, k) = kappa*h_face* &
                             (T_centre(i, j, k) - T_centre(i - 1, j, k))* &
                             idxCu(i, j)*dy_cu(i, j)
      end do
      ! z-level closed faces: a separate host-gated pass so the loop above
      ! is textually unchanged with the knob off.
      if (present(open_u)) then
         do concurrent(k=1:nz, j=1:ny, i=2:nx)
            F_x_face(i, j, k) = F_x_face(i, j, k)*open_u(i, j, k)
         end do
      end if
      ! Array-bound faces: always zeroed (pass-4 divergence at the
      ! domain-edge cells reads them even when the physical wall is
      ! elsewhere inside the ghost band).
      do concurrent(k=1:nz, j=1:ny)
         F_x_face(1, j, k) = 0.0_wp
         F_x_face(nx + 1, j, k) = 0.0_wp
      end do
      ! Physical wall faces: zero only when the edge is actually a
      ! closed wall here (single-rank all-wall default; OBC/MPI-seam
      ! edges keep the computed flux read from a correctly-filled
      ! ghost column — see `tracer_hdiff`).
      do concurrent(k=1:nz, j=1:ny)
         if (wall_w) F_x_face(nghost + 1, j, k) = 0.0_wp
         if (wall_e) F_x_face(nghost + nx_phys + 1, j, k) = 0.0_wp
      end do

      ! ---- Pass 3: north-face transport flux ----
      do concurrent(k=1:nz, j=2:ny, i=1:nx) local(h_face)
         h_face = 0.5_wp*(h(i, j - 1, k) + h(i, j, k))
         F_y_face(i, j, k) = kappa*h_face* &
                             (T_centre(i, j, k) - T_centre(i, j - 1, k))* &
                             idyCv(i, j)*dx_cv(i, j)
      end do
      ! z-level closed faces: see the zonal twin.
      if (present(open_v)) then
         do concurrent(k=1:nz, j=2:ny, i=1:nx)
            F_y_face(i, j, k) = F_y_face(i, j, k)*open_v(i, j, k)
         end do
      end if
      do concurrent(k=1:nz, i=1:nx)
         F_y_face(i, 1, k) = 0.0_wp
         F_y_face(i, ny + 1, k) = 0.0_wp
      end do
      do concurrent(k=1:nz, i=1:nx)
         if (wall_s) F_y_face(i, nghost + 1, k) = 0.0_wp
         if (wall_n) F_y_face(i, nghost + ny_phys + 1, k) = 0.0_wp
      end do

      ! ---- Pass 4: apply area-weighted divergence ----
      if (present(budget)) then
         do concurrent(k=1:nz, j=1:ny, i=1:nx) local(delta)
            delta = dt*( &
                    (F_x_face(i + 1, j, k) - F_x_face(i, j, k)) + &
                    (F_y_face(i, j + 1, k) - F_y_face(i, j, k)))*iareaT(i, j)
            hTr(i, j, k) = hTr(i, j, k) + delta
            budget(i, j, k) = budget(i, j, k) + delta
         end do
      else
         do concurrent(k=1:nz, j=1:ny, i=1:nx)
            hTr(i, j, k) = hTr(i, j, k) + dt*( &
                           (F_x_face(i + 1, j, k) - F_x_face(i, j, k)) + &
                           (F_y_face(i, j + 1, k) - F_y_face(i, j, k)))*iareaT(i, j)
         end do
      end if
   end subroutine tracer_hdiff_one_impl

   pure function ocean_hdiff_tracer_bytes(this) result(nbytes)
      !! Counted allocatable footprint of the horizontal tracer diffusion slot (0 when
      !! unallocated).
      class(ocean_hdiff_tracer_t), intent(in) :: this
      integer(int64) :: nbytes
      nbytes = this%T_centre%bytes() &
               + this%F_x_face%bytes() &
               + this%F_y_face%bytes()
   end function ocean_hdiff_tracer_bytes

end module rdb_ocean_hdiff_tracer
