/******************************************************************************
 * Copyright (c) 2024 Lawrence Livermore National Security, LLC and other
 * HYPRE Project Developers. See the top-level COPYRIGHT file for details.
 *
 * SPDX-License-Identifier: MIT
 ******************************************************************************/

#include "internal/precon.h"
#include "HYPRE_parcsr_mv.h"
#include "_hypre_parcsr_mv.h"
#include "internal/gen_macros.h"
#include "internal/krylov.h"
#include "logging.h"

#include <stdio.h>

#if !HYPRE_CHECK_MIN_VERSION(22500, 0)
static HYPRE_Int
PreconUnavailable(HYPRE_Solver solver, HYPRE_ParCSRMatrix A, HYPRE_ParVector b,
                  HYPRE_ParVector x)
{
   (void)solver;
   (void)A;
   (void)b;
   (void)x;
   return 1;
}
#endif

#if HYPRE_CHECK_MIN_VERSION(30100, 55)
#define HYPREDRV_PRECON_SCHWARZ_FIELD(_prefix) \
   ADD_FIELD_OFFSET_ENTRY(_prefix, schwarz, hypredrv_SchwarzSetArgs)
#else
#define HYPREDRV_PRECON_SCHWARZ_FIELD(_prefix)
#endif

#define precon_FIELDS(_prefix)                                 \
   ADD_FIELD_OFFSET_ENTRY(_prefix, amg, hypredrv_AMGSetArgs)   \
   ADD_FIELD_OFFSET_ENTRY(_prefix, mgr, hypredrv_MGRSetArgs)   \
   ADD_FIELD_OFFSET_ENTRY(_prefix, ilu, hypredrv_ILUSetArgs)   \
   ADD_FIELD_OFFSET_ENTRY(_prefix, fsai, hypredrv_FSAISetArgs) \
   ADD_FIELD_OFFSET_ENTRY(_prefix, ams, hypredrv_AMSSetArgs)   \
   ADD_FIELD_OFFSET_ENTRY(_prefix, ads, hypredrv_ADSSetArgs)   \
   HYPREDRV_PRECON_SCHWARZ_FIELD(_prefix)                      \
   ADD_FIELD_OFFSET_ENTRY(_prefix, reuse, hypredrv_FieldTypeIntSet)

/* GCOVR_EXCL_START */
DEFINE_FIELD_OFFSET_MAP(precon)
#define precon_NUM_FIELDS \
   (sizeof(precon_field_offset_map) / sizeof(precon_field_offset_map[0]))

DEFINE_SET_FIELD_BY_NAME_FUNC(hypredrv_PreconSetFieldByName, precon_args,
                              precon_field_offset_map, precon_NUM_FIELDS)
DEFINE_GET_VALID_KEYS_FUNC(hypredrv_PreconGetValidKeys, precon_NUM_FIELDS,
                           precon_field_offset_map)
/* GCOVR_EXCL_STOP */

/*-----------------------------------------------------------------------------
 * hypredrv_PreconGetValidValues
 *-----------------------------------------------------------------------------*/

StrIntMapArray
hypredrv_PreconGetValidValues(const char *key)
{
   (void)key;
   /* The "preconditioner" entry does not hold values, so we create a void map */
   return STR_INT_MAP_ARRAY_VOID();
}

/*-----------------------------------------------------------------------------
 * hypredrv_PreconGetValidTypeIntMap
 *-----------------------------------------------------------------------------*/

StrIntMapArray
hypredrv_PreconGetValidTypeIntMap(void)
{
   static StrIntMap map[] = {
      {"amg", (int)PRECON_BOOMERAMG},
      {"jacobi", (int)PRECON_BOOMERAMG},
      {"gauss-seidel", (int)PRECON_BOOMERAMG},
      {"mgr", (int)PRECON_MGR},
      {"ilu", (int)PRECON_ILU},
      {"fsai", (int)PRECON_FSAI},
      {"ams", (int)PRECON_AMS},
      {"ads", (int)PRECON_ADS},
#if HYPRE_CHECK_MIN_VERSION(30100, 55)
      {"schwarz", (int)PRECON_SCHWARZ},
#endif
   };

   return STR_INT_MAP_ARRAY_CREATE(map);
}

/* Per-method hypre entry points. Index by precon_t; keep in sync with the enum. */
typedef HYPRE_Int (*PreconParFn)(HYPRE_Solver, HYPRE_ParCSRMatrix, HYPRE_ParVector,
                                 HYPRE_ParVector);
typedef HYPRE_Int (*PreconDestroyFn)(HYPRE_Solver);

typedef struct PreconOps_struct
{
   PreconParFn     setup;
   PreconParFn     solve;
   PreconDestroyFn destroy;
} PreconOps;

static const PreconOps precon_ops[] = {
   [PRECON_BOOMERAMG] = {.setup   = HYPRE_BoomerAMGSetup,
                         .solve   = HYPRE_BoomerAMGSolve,
                         .destroy = HYPRE_BoomerAMGDestroy},
   [PRECON_MGR] =
      {
#if HYPRE_CHECK_MIN_VERSION(21900, 0)
         .setup = HYPRE_MGRSetup,
         .solve = HYPRE_MGRSolve,
#else
         .setup = NULL,
         .solve = NULL,
#endif
         .destroy = NULL, /* MGR teardown is handled by PreconDestroyMGRSolver */
      },
   [PRECON_ILU] =
      {
#if HYPRE_CHECK_MIN_VERSION(21900, 0)
         .setup   = HYPRE_ILUSetup,
         .solve   = HYPRE_ILUSolve,
         .destroy = HYPRE_ILUDestroy,
#else
         .setup   = NULL,
         .solve   = NULL,
         .destroy = NULL,
#endif
      },
   [PRECON_FSAI] =
      {
#if HYPRE_CHECK_MIN_VERSION(22500, 0)
         .setup   = HYPRE_FSAISetup,
         .solve   = HYPRE_FSAISolve,
         .destroy = HYPRE_FSAIDestroy,
#else
         .setup   = NULL,
         .solve   = NULL,
         .destroy = NULL,
#endif
      },
   [PRECON_AMS] = {.setup   = HYPRE_AMSSetup,
                   .solve   = HYPRE_AMSSolve,
                   .destroy = HYPRE_AMSDestroy},
   [PRECON_ADS] = {.setup   = HYPRE_ADSSetup,
                   .solve   = HYPRE_ADSSolve,
                   .destroy = HYPRE_ADSDestroy},
#if HYPRE_CHECK_MIN_VERSION(30100, 55)
   [PRECON_SCHWARZ] = {.setup   = HYPRE_SchwarzSetup,
                       .solve   = HYPRE_SchwarzSolve,
                       .destroy = HYPRE_SchwarzDestroy},
#endif
   [PRECON_NONE] = {.setup = NULL, .solve = NULL, .destroy = NULL},
};

static const PreconOps *
PreconOpsLookup(precon_t method)
{
   if ((unsigned)method >= (unsigned)(sizeof(precon_ops) / sizeof(precon_ops[0])))
   {
      return NULL;
   }

   return &precon_ops[method];
}

void
hypredrv_PreconGetCallbacks(precon_t method, HYPRE_PtrToParSolverFcn *setup,
                            HYPRE_PtrToParSolverFcn *solve)
{
   const PreconOps        *ops      = PreconOpsLookup(method);
   HYPRE_PtrToParSolverFcn setup_fn = NULL;
   HYPRE_PtrToParSolverFcn solve_fn = NULL;

   if (ops)
   {
      setup_fn = ops->setup;
      solve_fn = ops->solve;
   }

#if !HYPRE_CHECK_MIN_VERSION(22500, 0)
   /* Nested Krylov SetPrecond needs non-NULL callbacks even when the method is
    * unavailable on this hypre build; the stub returns an error at call time. */
   if ((!setup_fn || !solve_fn) && (method == PRECON_ILU || method == PRECON_FSAI))
   {
      setup_fn = PreconUnavailable;
      solve_fn = PreconUnavailable;
   }
#endif

   if (setup)
   {
      *setup = setup_fn;
   }
   if (solve)
   {
      *solve = solve_fn;
   }
}

/*-----------------------------------------------------------------------------
 * hypredrv_PreconSetDefaultArgs
 *-----------------------------------------------------------------------------*/

void
hypredrv_PreconSetDefaultArgs(precon_args *args)
{
   args->reuse = 0;
}

void
hypredrv_PreconArgsSetDefaultsForMethod(precon_t method, precon_args *args)
{
   if (!args)
   {
      return;
   }

   hypredrv_PreconSetDefaultArgs(args);

   switch (method)
   {
      case PRECON_BOOMERAMG:
         hypredrv_AMGSetDefaultArgs(&args->amg);
         break;
      case PRECON_MGR:
         hypredrv_MGRSetDefaultArgs(&args->mgr);
         break;
      case PRECON_ILU:
         hypredrv_ILUSetDefaultArgs(&args->ilu);
         break;
      case PRECON_FSAI:
         hypredrv_FSAISetDefaultArgs(&args->fsai);
         break;
      case PRECON_AMS:
         hypredrv_AMSSetDefaultArgs(&args->ams);
         break;
      case PRECON_ADS:
         hypredrv_ADSSetDefaultArgs(&args->ads);
         break;
#if HYPRE_CHECK_MIN_VERSION(30100, 55)
      case PRECON_SCHWARZ:
         hypredrv_SchwarzSetDefaultArgs(&args->schwarz);
         break;
#endif
      case PRECON_NONE:
      default:
         break;
   }
}

void
hypredrv_PreconArgsSetDefaultsForName(precon_t method, const char *name,
                                      precon_args *args)
{
   hypredrv_PreconArgsSetDefaultsForMethod(method, args);

   if (!args || !name || method != PRECON_BOOMERAMG)
   {
      return;
   }

   HYPRE_Int relax_type = -1;
   if (!strcmp(name, "jacobi"))
   {
      relax_type = 0;
   }
   else if (!strcmp(name, "gauss-seidel"))
   {
      relax_type = 3;
   }

   if (relax_type < 0)
   {
      return;
   }

   args->amg.coarsening.max_levels    = 1;
   args->amg.relaxation.type          = relax_type;
   args->amg.relaxation.down_type     = relax_type;
   args->amg.relaxation.coarse_type   = relax_type;
   args->amg.relaxation.down_sweeps   = 1;
   args->amg.relaxation.up_sweeps     = 0;
   args->amg.relaxation.coarse_sweeps = 1;
}

void
hypredrv_PreconArgsDestroyOwnedConfig(precon_t method, precon_args *args)
{
   if (!args)
   {
      return;
   }

   switch (method)
   {
      case PRECON_MGR:
         hypredrv_MGRDestroyNestedSolverArgs(&args->mgr);
         break;

      case PRECON_BOOMERAMG:
      case PRECON_ILU:
      case PRECON_FSAI:
      case PRECON_AMS:
      case PRECON_ADS:
#if HYPRE_CHECK_MIN_VERSION(30100, 55)
      case PRECON_SCHWARZ:
#endif
      case PRECON_NONE:
      default:
         break;
   }
}

void
hypredrv_PreconArgsDestroyRuntimeState(precon_t method, precon_args *args)
{
   if (!args)
   {
      return;
   }

   switch (method)
   {
      case PRECON_MGR:
         hypredrv_MGRDestroyCachedSolvers(&args->mgr, 0);
         hypredrv_MGRForgetCachedSolvers(&args->mgr);
         break;

      case PRECON_BOOMERAMG:
      case PRECON_ILU:
      case PRECON_FSAI:
      case PRECON_AMS:
      case PRECON_ADS:
#if HYPRE_CHECK_MIN_VERSION(30100, 55)
      case PRECON_SCHWARZ:
#endif
      case PRECON_NONE:
      default:
         break;
   }
}

static int PreconHasConfiguredComponentReuse(precon_t method, const precon_args *args);

static int
NestedKrylovHasConfiguredComponentReuse(const NestedKrylov_args *args)
{
   if (!args || !args->has_precon)
   {
      return 0;
   }

   return PreconHasConfiguredComponentReuse(args->precon_method, &args->precon);
}

static int
MGRHasConfiguredComponentReuse(const MGR_args *args)
{
   if (!args)
   {
      return 0;
   }

   if (args->coarsest_level.reuse.present)
   {
      return 1;
   }

   if (args->coarsest_level.use_krylov &&
       NestedKrylovHasConfiguredComponentReuse(args->coarsest_level.krylov))
   {
      return 1;
   }

   int max_levels = (args->num_levels > 0) ? (args->num_levels - 1) : 0;
   for (int i = 0; i < max_levels; i++)
   {
      const MGRlvl_args *level_args = &args->level[i];

      if (level_args->f_relaxation.reuse.present ||
          level_args->g_relaxation.reuse.present)
      {
         return 1;
      }

      if (level_args->f_relaxation.use_krylov &&
          NestedKrylovHasConfiguredComponentReuse(level_args->f_relaxation.krylov))
      {
         return 1;
      }

      if (level_args->g_relaxation.use_krylov &&
          NestedKrylovHasConfiguredComponentReuse(level_args->g_relaxation.krylov))
      {
         return 1;
      }

      if (level_args->f_relaxation.mgr &&
          MGRHasConfiguredComponentReuse(level_args->f_relaxation.mgr))
      {
         return 1;
      }
   }

   return 0;
}

static int
PreconHasConfiguredComponentReuse(precon_t method, const precon_args *args)
{
   if (!args)
   {
      return 0;
   }

   switch (method)
   {
      case PRECON_MGR:
         return MGRHasConfiguredComponentReuse(&args->mgr);

      case PRECON_BOOMERAMG:
      case PRECON_ILU:
      case PRECON_FSAI:
      case PRECON_AMS:
      case PRECON_ADS:
#if HYPRE_CHECK_MIN_VERSION(30100, 55)
      case PRECON_SCHWARZ:
#endif
      case PRECON_NONE:
      default:
         return 0;
   }
}

/*-----------------------------------------------------------------------------
 * hypredrv_PreconSetArgsFromYAML
 *-----------------------------------------------------------------------------*/

void
hypredrv_PreconSetArgsFromYAML(precon_args *args, YAMLnode *parent)
{
   if (!parent || !parent->children)
   {
      return;
   }

   hypredrv_YAMLSetArgsGeneric((void *)args, parent, hypredrv_PreconGetValidKeys,
                               hypredrv_PreconGetValidValues,
                               hypredrv_PreconSetFieldByName);
}

/*-----------------------------------------------------------------------------
 * hypredrv_PreconCreate
 *-----------------------------------------------------------------------------*/

void
hypredrv_PreconCreate(precon_t precon_method, precon_args *args, IntArray *dofmap,
                      HYPRE_IJVector vec_nn, HYPRE_Precon *precon_ptr, const Stats *stats,
                      int next_ls_id, const PreconOperators *ops)
{
   if (!PreconHasConfiguredComponentReuse(precon_method, args))
   {
      hypredrv_PreconArgsDestroyRuntimeState(precon_method, args);
   }

   HYPRE_Precon precon = malloc(sizeof(hypre_Precon)); /* GCOVR_EXCL_BR_LINE */
   if (!precon)                                        /* GCOVR_EXCL_BR_LINE */
   {
      hypredrv_ErrorCodeSet(ERROR_ALLOCATION);
      *precon_ptr = NULL;
      return;
   }

   precon->main     = NULL;
   precon->method   = precon_method;
   precon->is_setup = 0;
   /* Methods that need no operator inputs are always ready for setup; AMS/ADS
    * are only ready when every required operator was supplied (checked below). */
   precon->operators_ok = hypredrv_PreconOperatorsComplete(precon_method, ops);
   precon->stats        = NULL;

   switch (precon_method)
   {
      case PRECON_BOOMERAMG:
         /* GM1 marks modes projected to an MGR subblock. Full-system vectors would
          * not match that block's partitioning during interpolation setup. */
         if (args->amg.interp_vec_variant != 1)
         {
            hypredrv_AMGSetRBMs(&args->amg, vec_nn);
         }
         hypredrv_AMGCreate(&args->amg, &precon->main);
         break;

      case PRECON_MGR:
         hypredrv_MGRSetDofmap(&args->mgr, dofmap);
         hypredrv_MGRSetNearNullSpace(&args->mgr, vec_nn);
         hypredrv_MGRSetCoarseSchur(&args->mgr, ops ? ops->coarse_schur : NULL);
         hypredrv_MGRCreate(&args->mgr, &precon->main, stats, next_ls_id);
         break;

      case PRECON_ILU:
         hypredrv_ILUCreate(&args->ilu, &precon->main);
         break;

      case PRECON_FSAI:
         hypredrv_FSAICreate(&args->fsai, &precon->main);
         break;

      case PRECON_AMS:
         hypredrv_AMSCreate(&args->ams, &precon->main);
         if (ops)
         {
            hypredrv_AMSSetOperators(precon->main, ops->G, ops->coord[0], ops->coord[1],
                                     ops->coord[2]);
         }
         break;

      case PRECON_ADS:
         hypredrv_ADSCreate(&args->ads, &precon->main);
         if (ops)
         {
            hypredrv_ADSSetOperators(precon->main, ops->G, ops->C, ops->coord[0],
                                     ops->coord[1], ops->coord[2]);
         }
         break;

#if HYPRE_CHECK_MIN_VERSION(30100, 55)
      case PRECON_SCHWARZ:
         hypredrv_SchwarzCreate(&args->schwarz, &precon->main);
         break;
#endif

      case PRECON_NONE:
         break;

      default:
         hypredrv_ErrorCodeSet(ERROR_INVALID_PRECON);
         free(precon);
         *precon_ptr = NULL;
         return;
   }

   /* If a sub-create failed, do not publish a half-built wrapper whose main handle
    * is NULL: a later PreconSetup/PreconApply would otherwise pass NULL straight
    * into hypre and crash. A NULL main for any method other than PRECON_NONE is the
    * reliable failure signal here (the process-global error state is sticky and may
    * reflect an unrelated earlier failure, so it must not be used for this check). */
   if (precon_method != PRECON_NONE && !precon->main)
   {
      if (!hypredrv_ErrorCodeActive())
      {
         hypredrv_ErrorCodeSet(ERROR_INVALID_PRECON);
         hypredrv_ErrorMsgAdd("Preconditioner creation failed (method=%d)",
                              (int)precon_method);
      }
      hypredrv_PreconArgsDestroyRuntimeState(precon_method, args);
      free(precon);
      *precon_ptr = NULL;
      return;
   }

   *precon_ptr = precon;
}

/*-----------------------------------------------------------------------------
 * hypredrv_PreconMethodRequiresOperators
 *-----------------------------------------------------------------------------*/

int
hypredrv_PreconMethodRequiresOperators(precon_t precon_method)
{
   return (precon_method == PRECON_AMS || precon_method == PRECON_ADS);
}

/*-----------------------------------------------------------------------------
 * hypredrv_PreconSupportsDevice
 *-----------------------------------------------------------------------------*/

int
hypredrv_PreconSupportsDevice(precon_t precon_method, const precon_args *args,
                              char *reason, size_t reason_size)
{
   if (!args)
   {
      return 1;
   }

   if (precon_method == PRECON_BOOMERAMG)
   {
      int interp_type = args->amg.interpolation.prolongation_type;
      if (interp_type == 8 || interp_type == 9)
      {
         if (reason && reason_size)
         {
            snprintf(reason, reason_size,
                     "BoomerAMG interpolation type %d is not ported to GPUs",
                     interp_type);
         }
         return 0;
      }
   }

#if HYPRE_CHECK_MIN_VERSION(30100, 55)
   if (precon_method == PRECON_MGR)
   {
      int fine_levels = args->mgr.num_levels > 0 ? args->mgr.num_levels - 1 : 0;
      for (int level = 0; level < fine_levels; level++)
      {
         if (args->mgr.level[level].f_relaxation.type == MGR_SOLVER_TYPE_SCHWARZ)
         {
            if (reason && reason_size)
            {
               snprintf(reason, reason_size,
                        "MGR level %d Schwarz F-relaxation is not ported to GPUs", level);
            }
            return 0;
         }
         if (args->mgr.level[level].g_relaxation.type == MGR_SOLVER_TYPE_SCHWARZ)
         {
            if (reason && reason_size)
            {
               snprintf(reason, reason_size,
                        "MGR level %d Schwarz global relaxation is not ported to GPUs",
                        level);
            }
            return 0;
         }
      }
   }
#endif

   if (reason && reason_size)
   {
      reason[0] = '\0';
   }
   return 1;
}

/*-----------------------------------------------------------------------------
 * hypredrv_PreconOperatorsComplete
 *
 * AMS needs a discrete gradient and the three coordinate vectors; ADS needs all
 * of that plus a discrete curl. These mirror the inputs hypredrv_AMSSetOperators
 * / hypredrv_ADSSetOperators forward to HYPRE_*SetDiscreteGradient /
 * HYPRE_*SetDiscreteCurl / HYPRE_*SetCoordinateVectors; if any is missing the
 * hypre setup routine dereferences NULL.
 *-----------------------------------------------------------------------------*/

int
hypredrv_PreconOperatorsComplete(precon_t precon_method, const PreconOperators *ops)
{
   if (!hypredrv_PreconMethodRequiresOperators(precon_method))
   {
      return 1;
   }

   if (!ops)
   {
      return 0;
   }

   int have_coords = (ops->coord[0] && ops->coord[1] && ops->coord[2]);

   if (precon_method == PRECON_AMS)
   {
      return (ops->G && have_coords) ? 1 : 0;
   }

   /* PRECON_ADS */
   return (ops->G && ops->C && have_coords) ? 1 : 0;
}

/*-----------------------------------------------------------------------------
 * hypredrv_PreconSetupOperatorGuard
 *-----------------------------------------------------------------------------*/

int
hypredrv_PreconSetupOperatorGuard(HYPRE_Precon precon)
{
   if (!precon || precon->operators_ok)
   {
      return 0;
   }

   if (!hypredrv_PreconMethodRequiresOperators(precon->method))
   {
      return 0;
   }

   hypredrv_ErrorCodeSet(ERROR_MISSING_PRECON);
   if (precon->method == PRECON_AMS)
   {
      hypredrv_ErrorMsgAdd("AMS setup requires a discrete gradient matrix and "
                           "coordinate vectors, but they were not provided");
   }
   else
   {
      hypredrv_ErrorMsgAdd("ADS setup requires a discrete gradient matrix, a discrete "
                           "curl matrix, and coordinate vectors, but they were not "
                           "provided");
   }

   return 1;
}

/*-----------------------------------------------------------------------------
 * hypredrv_PreconSetup
 *-----------------------------------------------------------------------------*/

void
hypredrv_PreconSetup(precon_t precon_method, HYPRE_Precon precon, HYPRE_IJMatrix A)
{
   void              *vA    = NULL;
   HYPRE_ParCSRMatrix par_A = NULL;
   HYPRE_ParVector    par_b = NULL, par_x = NULL;
   /* GCOVR_EXCL_BR_LINE */
   if (precon_method == PRECON_NONE) /* GCOVR_EXCL_BR_LINE */
   {
      return;
   }

   if (!precon || !precon->main)
   {
      hypredrv_ErrorCodeSet(ERROR_INVALID_PRECON);
      hypredrv_ErrorMsgAdd("Preconditioner setup requested with null preconditioner");
      return;
   }

   if (!A)
   {
      hypredrv_ErrorCodeSet(ERROR_INVALID_VAL);
      hypredrv_ErrorMsgAdd("Preconditioner setup requested with null matrix");
      return;
   }

   /* Abort cleanly (instead of letting hypre dereference NULL) when an AMS/ADS
    * preconditioner is set up without its required operator inputs. */
   if (hypredrv_PreconSetupOperatorGuard(precon))
   {
      return;
   }

   HYPRE_Solver     prec = precon->main;
   const PreconOps *ops  = PreconOpsLookup(precon_method);

   HYPRE_IJMatrixGetObject(A, &vA);
   par_A = (HYPRE_ParCSRMatrix)vA;

   if (!ops || !ops->setup)
   {
      hypredrv_ErrorCodeSet(ERROR_INVALID_PRECON);
      if (precon_method == PRECON_MGR)
      {
         hypredrv_ErrorMsgAdd("MGR requires hypre >= 2.19.0");
      }
      else if (precon_method == PRECON_ILU)
      {
         hypredrv_ErrorMsgAdd("ILU requires hypre >= 2.19.0");
      }
      else if (precon_method == PRECON_FSAI)
      {
         hypredrv_ErrorMsgAdd("FSAI requires hypre >= 2.25.0");
      }
      return;
   }

#if HYPREDRV_HYPRE_RELEASE_NUMBER == 22800
   /* hypre 2.28.0 unconditionally reads the RHS vector's component count during
    * ILU/FSAI setup, although setup otherwise accepts NULL vectors. */
   if (precon_method == PRECON_ILU || precon_method == PRECON_FSAI)
   {
      HYPRE_ParVectorCreate(hypre_ParCSRMatrixComm(par_A),
                            hypre_ParCSRMatrixGlobalNumRows(par_A), NULL, &par_b);
      HYPRE_ParVectorInitialize(par_b);
      par_x = par_b;
   }
#endif

   ops->setup(prec, par_A, par_b, par_x);

   hypredrv_HypreConsumeErrors();
#if HYPREDRV_HYPRE_RELEASE_NUMBER == 22800
   if (par_b)
   {
      HYPRE_ParVectorDestroy(par_b);
   }
#endif

   if (hypredrv_ErrorCodeActive())
   {
      precon->is_setup = 0;
      return;
   }

   precon->is_setup = 1;

   // TODO: fix timing. Adjust LinearSolverSetup.
   // StatsTimerStop("prec");
}

/*-----------------------------------------------------------------------------
 * hypredrv_PreconApply
 *-----------------------------------------------------------------------------*/

void
hypredrv_PreconApply(precon_t precon_method, HYPRE_Precon precon, HYPRE_IJMatrix A,
                     HYPRE_IJVector b, HYPRE_IJVector x)
{
   void              *vA = NULL, *vb = NULL, *vx = NULL;
   HYPRE_ParCSRMatrix par_A = NULL;
   HYPRE_ParVector    par_b = NULL, par_x = NULL;

   if (precon_method == PRECON_NONE)
   {
      return;
   }
   if (!precon || !precon->main)
   {
      hypredrv_ErrorCodeSet(ERROR_INVALID_PRECON);
      hypredrv_ErrorMsgAdd("Preconditioner apply requested with null preconditioner");
      return;
   }

   HYPRE_Solver     prec = precon->main;
   const PreconOps *ops  = PreconOpsLookup(precon_method);

   HYPRE_IJMatrixGetObject(A, &vA);
   par_A = (HYPRE_ParCSRMatrix)vA;
   HYPRE_IJVectorGetObject(b, &vb);
   par_b = (HYPRE_ParVector)vb;
   HYPRE_IJVectorGetObject(x, &vx);
   par_x = (HYPRE_ParVector)vx;

   if (!ops || !ops->solve)
   {
      hypredrv_ErrorCodeSet(ERROR_INVALID_PRECON);
      if (precon_method == PRECON_MGR)
      {
         hypredrv_ErrorMsgAdd("MGR requires hypre >= 2.19.0");
      }
      else if (precon_method == PRECON_ILU)
      {
         hypredrv_ErrorMsgAdd("ILU requires hypre >= 2.19.0");
      }
      else if (precon_method == PRECON_FSAI)
      {
         hypredrv_ErrorMsgAdd("FSAI requires hypre >= 2.25.0");
      }
      return;
   }

   ops->solve(prec, par_A, par_b, par_x);

   // StatsTimerStop("prec_apply");
}

static void
PreconDestroyMGRSolver(MGR_args *mgr, HYPRE_Solver *solver_ptr, int precon_was_setup)
{
#if !HYPRE_CHECK_MIN_VERSION(21900, 0)
   (void)mgr;
   (void)solver_ptr;
   (void)precon_was_setup;
   hypredrv_ErrorCodeSet(ERROR_INVALID_PRECON);
   hypredrv_ErrorMsgAdd("MGR requires hypre >= 2.19.0");
#else /* GCOVR_EXCL_BR_LINE */
   if (!mgr) /* GCOVR_EXCL_BR_LINE */
   {
      return;
   }
   /* GCOVR_EXCL_BR_LINE */
   if (!solver_ptr || !*solver_ptr) /* GCOVR_EXCL_BR_LINE */
   {
      /* Outer MGR solver was never created (e.g. early failure in MGRCreate).
       * Destroy or preserve any component handles that were set up before the
       * failure, respecting keep flags set by the caller. */
      hypredrv_MGRDestroyCachedSolvers(mgr, 0); /* GCOVR_EXCL_BR_LINE */
      if (mgr->point_marker_data)
      {
         free(mgr->point_marker_data);
         mgr->point_marker_data = NULL;
      }
      return;
   }

   if (!precon_was_setup)
   {
      /* Setup was never called, so refreshed handles may still be owned by
       * hypredrive only. Reclaim them before destroying the outer MGR object. */
      hypredrv_MGRDestroyCachedSolvers(mgr, 0);
      HYPRE_MGRDestroy(*solver_ptr);
      *solver_ptr = NULL;
   }
   else
   {
      HYPRE_MGRDestroy(*solver_ptr);
      *solver_ptr = NULL;
      /* Parent MGR is gone; clear any preserved cached-handle state without
       * destroying parent-owned internals a second time. */
      hypredrv_MGRDestroyCachedSolvers(mgr, 1);
   }

   if (mgr->point_marker_data)
   {
      free(mgr->point_marker_data);
      mgr->point_marker_data = NULL;
   }
#endif
}

/*-----------------------------------------------------------------------------
 * hypredrv_PreconDestroy
 *-----------------------------------------------------------------------------*/

void
hypredrv_PreconDestroy(precon_t precon_method, precon_args *args,
                       HYPRE_Precon *precon_ptr, const Stats *stats, int ls_id)
{
   int log_rank = -1;          /* GCOVR_EXCL_BR_LINE */
   if (hypredrv_LogEnabled(3)) /* GCOVR_EXCL_BR_LINE */
   {
      log_rank = hypredrv_LogRankFromComm(MPI_COMM_WORLD);
   }

   /* Resolve object name the same way hypredrv_LogObjectf does */
   const char *obj_name = NULL;
   char        obj_name_buf[32];
   obj_name_buf[0] = '\0';
   if (stats)
   {
      if (stats->object_name[0] != '\0')
      {
         obj_name = stats->object_name;
      }
      else if (stats->runtime_object_id > 0)
      {
         snprintf(obj_name_buf, sizeof(obj_name_buf), "obj-%d", stats->runtime_object_id);
         obj_name = obj_name_buf;
      }
   }

   HYPRE_Precon precon = *precon_ptr;
   /* GCOVR_EXCL_BR_LINE */
   if (!precon) /* GCOVR_EXCL_BR_LINE */
   {
      /* GCOVR_EXCL_START */
      HYPREDRV_LOGF(3, log_rank, obj_name, ls_id,
                    "preconditioner destroy skipped: object already NULL");
      /* GCOVR_EXCL_STOP */
      return;
   }

   /* Enter the dispatch when a main handle exists, and also for MGR even without a
    * main handle: PreconDestroyMGRSolver cleans up component solvers cached before
    * an outer-MGR create failure, which would otherwise leak. */
   if (precon->main || precon_method == PRECON_MGR)
   {
      switch (precon_method)
      {
         case PRECON_BOOMERAMG:
            /* GCOVR_EXCL_START */
            HYPREDRV_LOGF(3, log_rank, obj_name, ls_id,
                          "preconditioner destroy dispatch: method=boomeramg");
            /* GCOVR_EXCL_STOP */
            HYPRE_BoomerAMGDestroy(precon->main);
            hypredrv_AMGDestroyRBMs(&args->amg);
            break;

         case PRECON_MGR:
            /* GCOVR_EXCL_START */
            HYPREDRV_LOGF(3, log_rank, obj_name, ls_id,
                          "preconditioner destroy dispatch: method=mgr");
            /* GCOVR_EXCL_STOP */
            PreconDestroyMGRSolver(&args->mgr, &precon->main, precon->is_setup);
            break;

         case PRECON_ILU:
            /* GCOVR_EXCL_START */
            HYPREDRV_LOGF(3, log_rank, obj_name, ls_id,
                          "preconditioner destroy dispatch: method=ilu");
            /* GCOVR_EXCL_STOP */
#if HYPRE_CHECK_MIN_VERSION(21900, 0)
            HYPRE_ILUDestroy(precon->main);
#endif
            break;

         case PRECON_FSAI:
            /* GCOVR_EXCL_START */
            HYPREDRV_LOGF(3, log_rank, obj_name, ls_id,
                          "preconditioner destroy dispatch: method=fsai");
            /* GCOVR_EXCL_STOP */
#if HYPRE_CHECK_MIN_VERSION(22500, 0)
            HYPRE_FSAIDestroy(precon->main);
#endif
            break;

         case PRECON_AMS:
            /* GCOVR_EXCL_START */
            HYPREDRV_LOGF(3, log_rank, obj_name, ls_id,
                          "preconditioner destroy dispatch: method=ams");
            /* GCOVR_EXCL_STOP */
            HYPRE_AMSDestroy(precon->main);
            break;

         case PRECON_ADS:
            /* GCOVR_EXCL_START */
            HYPREDRV_LOGF(3, log_rank, obj_name, ls_id,
                          "preconditioner destroy dispatch: method=ads");
            /* GCOVR_EXCL_STOP */
            HYPRE_ADSDestroy(precon->main);
            break;

#if HYPRE_CHECK_MIN_VERSION(30100, 55)
         case PRECON_SCHWARZ:
            /* GCOVR_EXCL_START */
            HYPREDRV_LOGF(3, log_rank, obj_name, ls_id,
                          "preconditioner destroy dispatch: method=schwarz");
            /* GCOVR_EXCL_STOP */
            HYPRE_SchwarzDestroy(precon->main);
            break;
#endif

         case PRECON_NONE:
            /* GCOVR_EXCL_START */
            HYPREDRV_LOGF(3, log_rank, obj_name, ls_id,
                          "preconditioner destroy dispatch: method=none");
            /* GCOVR_EXCL_STOP */
            break;
      }

      precon->main = NULL;
   }

   free(*precon_ptr);
   *precon_ptr = NULL;
}
