// CelerisLab – step/one_step_esopull.cu
// Esoteric-Pull single-buffer step kernel.
// Included by kernel_v2.cu.  No standalone compilation.
//
// CURVED BC LIMITATION: EsoPull maintains only ONE DDF buffer, so there is
// no "previous-step" fi_in buffer for Bouzidi interpolation (curved_boundary.cuh).
// Curved obstacle nodes therefore receive no boundary correction here — the
// apply_boundary_esopull helper returns early on is_curved().
// CurvedBoundaryKernel is SKIPPED by stepper.py when streaming==esopull.
//
// TODO(future): Support curved BC with EsoPull by maintaining a per-link
// shadow buffer (previous-step DDF for curved nodes only) or switching to
// a half-step strategy that reuses the in-place slot before overwrite.
// ============================================================================

#ifndef CELERIS_STEP_ONE_STEP_ESOPULL_CU
#define CELERIS_STEP_ONE_STEP_ESOPULL_CU

// ---- Helper: apply inlet/outlet BC in esopull mode ----
#if DIM == 2
__device__ __forceinline__ void apply_boundary_esopull(
    float* f, uint16_t fl, unsigned int x, unsigned int y,
    const fpxx* fi, unsigned long t)
{
    // Curved-BC nodes are handled by CurvedBoundaryKernel before the main step when
    // streaming is double_buffer; EsoPull skips curved (see stepper).
    if (is_curved(fl)) return;

    bool interior_y = (y > 0u) && (y < (unsigned int)(NY - 1));

    if (is_inlet(fl) && interior_y) {
        unsigned long k_neb = linear_index(x + 1u, y);
        unsigned long j_neb[NQ];
        compute_neighbors(x + 1u, y, j_neb);
        float f_neb[NQ];
        load_f_esopull(k_neb, f_neb, fi, j_neb, t);
        apply_parabolic_inlet(f, f_neb, (float)y);
    }
    else if (is_outlet(fl) && interior_y) {
        unsigned long k_neb = linear_index(x - 1u, y);
        unsigned long j_neb[NQ];
        compute_neighbors(x - 1u, y, j_neb);
        float f_neb[NQ];
        load_f_esopull(k_neb, f_neb, fi, j_neb, t);
        apply_pressure_outlet(f, f_neb, (float)y);
    }
    else {
        bounce_back_swap(f);
    }
}
#endif // DIM == 2

#if DIM == 3
__device__ __forceinline__ void apply_boundary_esopull_3d(
    float* f, uint16_t fl, unsigned int x, unsigned int y, unsigned int z,
    const fpxx* fi, unsigned long t)
{
    // Curved-BC nodes are handled by CurvedBoundaryKernel before the main step when
    // streaming is double_buffer; EsoPull skips curved (see stepper).
    if (is_curved(fl)) return;

    bool interior_y = (y > 0u) && (y < (unsigned int)(NY - 1));

    if (is_inlet(fl) && interior_y) {
        unsigned long k_neb = linear_index(x + 1u, y, z);
        unsigned long j_neb[NQ];
        compute_neighbors(k_neb, j_neb);
        float f_neb[NQ];
        load_f_esopull(k_neb, f_neb, fi, j_neb, t);
        apply_parabolic_inlet_3d(f, f_neb, (float)y);
    }
    else if (is_outlet(fl) && interior_y) {
        unsigned long k_neb = linear_index(x - 1u, y, z);
        unsigned long j_neb[NQ];
        compute_neighbors(k_neb, j_neb);
        float f_neb[NQ];
        load_f_esopull(k_neb, f_neb, fi, j_neb, t);
        apply_pressure_outlet_3d(f, f_neb, (float)y);
    }
    else {
        bounce_back_swap(f);
    }
}
#endif

// ============================================================================
// EsoPullStep — Esoteric-Pull single-buffer step kernel
// ============================================================================
__global__ __launch_bounds__(NT, 2)
void EsoPullStep(
    fpxx* fi, uint16_t* flag,
    float* action, float* obs,
    unsigned long t)
{
#if DIM == 2
    unsigned int x, y; unsigned long k;
    index_from_thread(x, y, k);
    if (x >= (unsigned int)NX || y >= (unsigned int)NY) return;

    uint16_t fl = flag[k];
    unsigned long j[NQ];
    compute_neighbors(x, y, j);

    float f[NQ];
    load_f_esopull(k, f, fi, j, t);

    // Pure solid early exit
    if (is_solid(fl) && !has_bc(fl)) {
        bounce_back_swap(f);
        store_f_esopull(k, f, fi, j, t);
        return;
    }

    // Boundary nodes
    if (is_solid(fl))
        apply_boundary_esopull(f, fl, x, y, fi, t);

    if (is_obstacle(fl) && !is_curved(fl))
        bounce_back_swap(f);

    // Collision (fluid only)
    if (is_fluid(fl)) {
        float rho_n, ux, uy;
        compute_rho_u(f, rho_n, ux, uy);
        collide_dispatch(f, rho_n, ux, uy);
    }

    store_f_esopull(k, f, fi, j, t);

#elif DIM == 3
    unsigned int x, y, z; unsigned long k;
    index_from_thread(x, y, z, k);
    if (x >= (unsigned int)NX || y >= (unsigned int)NY || z >= (unsigned int)NZ) return;

    uint16_t fl = flag[k];
    unsigned long j[NQ];
    compute_neighbors(k, j);

    float f[NQ];
    load_f_esopull(k, f, fi, j, t);

    if (is_solid(fl) && !has_bc(fl)) {
        bounce_back_swap(f);
        store_f_esopull(k, f, fi, j, t);
        return;
    }

    if (is_solid(fl))
        apply_boundary_esopull_3d(f, fl, x, y, z, fi, t);

    if (is_obstacle(fl) && !is_curved(fl))
        bounce_back_swap(f);

    if (is_fluid(fl)) {
        float rho_n, ux, uy, uz;
        compute_rho_u(f, rho_n, ux, uy, uz);
        collide_dispatch(f, rho_n, ux, uy, uz);
    }

    store_f_esopull(k, f, fi, j, t);
#endif
}

#endif // CELERIS_STEP_ONE_STEP_ESOPULL_CU
