// -*- mode: c++; c-basic-offset: 2; tab-width: 2; indent-tabs-mode: nil; -*-
#ifndef R_NO_REMAP
#define R_NO_REMAP
#endif
#ifndef __PAR_SOLVE_H___
#define __PAR_SOLVE_H___

#define isSameTimeOp(xout, xp) (op->stiff == 0 ? isSameTimeDop(xout, xp) : isSameTime(xout, xp))
#ifndef min2
#define min2( a , b )  ( (a) < (b) ? (a) : (b) )
#endif

#if defined(__cplusplus)

extern "C" {
#endif

#include "../inst/include/rxode2.h"
#include "rxThreadData.h"

void par_rkf78(rx_solve *rx);
extern "C" void ind_rkf78(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);
void par_trapz(rx_solve *rx);
extern "C" void ind_trapz(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);
extern "C" void trapz_solveWith1Pt(int *neq, double *yp, double *xp, double xout, int *istate, rx_solving_options *op, rx_solving_options_ind *ind);
void par_ssp3(rx_solve *rx);
extern "C" void ind_ssp3(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);
extern "C" void ssp3_solveWith1Pt(int *neq, double *yp, double *xp, double xout, int *istate, rx_solving_options *op, rx_solving_options_ind *ind);
void par_rkf32(rx_solve *rx);
extern "C" void ind_rkf32(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);
extern "C" void rkf32_solveWith1Pt(int *neq, double *yp, double *xp, double xout, int *istate, rx_solving_options *op, rx_solving_options_ind *ind);
void par_rk43(rx_solve *rx);
extern "C" void ind_rk43(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);
extern "C" void rk43_solveWith1Pt(int *neq, double *yp, double *xp, double xout, int *istate, rx_solving_options *op, rx_solving_options_ind *ind);
void par_dop54(rx_solve *rx);
extern "C" void ind_dop54(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);
extern "C" void dop54_solveWith1Pt(int *neq, double *yp, double *xp, double xout, int *istate, rx_solving_options *op, rx_solving_options_ind *ind);
void par_vern65(rx_solve *rx);
extern "C" void ind_vern65(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);
extern "C" void vern65_solveWith1Pt(int *neq, double *yp, double *xp, double xout, int *istate, rx_solving_options *op, rx_solving_options_ind *ind);
void par_vern76(rx_solve *rx);
extern "C" void ind_vern76(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);
extern "C" void vern76_solveWith1Pt(int *neq, double *yp, double *xp, double xout, int *istate, rx_solving_options *op, rx_solving_options_ind *ind);
void par_dop87(rx_solve *rx);
extern "C" void ind_dop87(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);
extern "C" void dop87_solveWith1Pt(int *neq, double *yp, double *xp, double xout, int *istate, rx_solving_options *op, rx_solving_options_ind *ind);
void par_vern98(rx_solve *rx);
extern "C" void ind_vern98(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);
extern "C" void vern98_solveWith1Pt(int *neq, double *yp, double *xp, double xout, int *istate, rx_solving_options *op, rx_solving_options_ind *ind);
void par_rk4(rx_solve *rx);
extern "C" void ind_rk4(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);
void par_rk4s(rx_solve *rx);
extern "C" void ind_rk4s(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);
extern "C" void par_cvodesadj(rx_solve *rx);
extern "C" void ind_cvodesadj(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);
extern "C" void par_liblsodaadj(rx_solve *rx);
extern "C" void ind_liblsodaadj(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);
extern "C" void par_ab_adj(rx_solve *rx);
extern "C" void ind_ab_adj(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);
void par_ck54(rx_solve *rx);
extern "C" void ind_ck54(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);
void par_ab(rx_solve *rx);
extern "C" void ind_ab(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);
void par_abm(rx_solve *rx);
extern "C" void ind_abm(rx_solve *rx, int solveid,
                        t_dydt c_dydt, t_update_inis u_inis);

extern "C" void par_dop5(rx_solve *rx);

extern "C" void ind_dop5(rx_solve *rx, int solveid,
                         t_dydt c_dydt, t_update_inis u_inis);

extern "C" void par_bs(rx_solve *rx);

extern "C" void ind_bs(rx_solve *rx, int solveid,
                         t_dydt c_dydt, t_update_inis u_inis);

extern "C" void par_ros4(rx_solve *rx);
extern "C" void ind_ros4(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);

extern "C" void par_dros4(rx_solve *rx);
extern "C" void ind_dros4(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);

extern "C" void par_iem(rx_solve *rx);
extern "C" void ind_iem(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);

extern "C" void par_sem(rx_solve *rx);
extern "C" void ind_sem(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);

extern "C" void par_sb3a(rx_solve *rx);
extern "C" void ind_sb3a(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);

extern "C" void par_sb3am4(rx_solve *rx);
extern "C" void ind_sb3am4(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);

extern "C" void par_vv(rx_solve *rx);
extern "C" void ind_vv(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);

extern "C" void par_mm(rx_solve *rx);
extern "C" void ind_mm(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);

extern "C" void par_em(rx_solve *rx);
extern "C" void ind_em(rx_solve *rx, int solveid, t_dydt c_dydt, t_update_inis u_inis);

extern "C" void par_cvode(rx_solve *rx);
extern "C" void ind_cvode(rx_solve *rx, int solveid, t_update_inis u_inis);
extern "C" void cvode_solveWith1Pt(int *neq, double *yp, double *xp_ptr, double xout,
                                    int *istate, rx_solving_options *op,
                                    rx_solving_options_ind *ind, void *ctx_ptr);

	void sortInd(rx_solving_options_ind *ind);

  void _setIndPointersByThread(rx_solving_options_ind *ind);

  extern double maxAtolRtolFactor;


	static inline int iniSubject(int solveid, int inLhs, rx_solving_options_ind *ind, rx_solving_options *op, rx_solve *rx,
															 t_update_inis u_inis) {
		ind->_rxFlag=1;
    ind->fns = &(rx->fns);
    ind->op = op;
    ind->rx = rx;
    ind->linSS=0;
    ind->linSScmt=0;
    ind->linSSvar=0.0;
    ind->linSStau=0.0;
    ind->linSSbolusCmt=0;
    ind->linCmtAlast = NULL;
    ind->ssTime = NA_REAL;
    _setIndPointersByThread(ind);
    // Apply this individual's sticky tolerance factor to the thread-local
    // tolerance arrays.
    if (ind->atol2 != NULL) {
      for (int _i = op->neq; _i--;) {
        ind->atol2[_i]  = min2(ind->atol2[_i]  * ind->tolFactor, maxAtolRtolFactor);
        ind->rtol2[_i]  = min2(ind->rtol2[_i]  * ind->tolFactor, maxAtolRtolFactor);
        ind->ssAtol[_i] = min2(ind->ssAtol[_i] * ind->tolFactor, maxAtolRtolFactor);
        ind->ssRtol[_i] = min2(ind->ssRtol[_i] * ind->tolFactor, maxAtolRtolFactor);
      }
    }
		for (int i=rxLlikSaveSize*op->nLlik; i--;) {
			ind->llikSave[i] = 0.0;
		}
		ind->ixds = ind->idx = ind->_update_par_ptr_in = 0; // reset dosing
    ind->nPushedExtra = 0; // reset per-solve evid_() push counter
    // Reset n_all_times to the original event-table size for the ODE solve
    // pass.  _rxPushDose() grows n_all_times during the solve; the LHS output
    // pass (inLhs==1) must see the grown count so that pushed events that
    // interleave with original observations are not silently dropped.
    if (inLhs == 0) ind->n_all_times = ind->n_all_times_orig;
		ind->id=solveid;
		ind->cacheME=0;
		ind->curShift=0.0;
		ind->lastIsSs2 = false;
    ind->idxLow=0;
    ind->idxHi=0;
    // Event ("jump") sensitivities: clear any deferred non-dosed dtau jump so it
    // does not leak across subjects; keep the buffer zeroed when idle (the flush
    // in preSolve re-zeros it after each use).
    ind->esHasPending = 0;
    ind->esPendingTau = NA_REAL;
    if (ind->esPendingJump != NULL) {
      memset(ind->esPendingJump, 0, rxEffNeq(ind, op)*sizeof(double));
    }
		// neq[0] = op->neq
    int ncmt = (op->neq + op->extraCmt);
		for (int j = ncmt; j--;) {
			ind->InfusionRate[j] = 0;
			ind->on[j] = 1;
			ind->tlastS[j] = NA_REAL;
			ind->tfirstS[j] = NA_REAL;
			ind->curDoseS[j] = NA_REAL;
		}
		ind->inLhs = inLhs;
		for (int j = op->nlhs; j--;) {
      if (op->lhs_str[j] == 1) {
        ind->lhs[j] = 1.0; // default is first string defined
      } else {
        ind->lhs[j] = NA_REAL;
      }
    }
		if ((inLhs == 0 && op->neq > 0) ||
				(inLhs == 1 && op->neq == 0 && (rx->nIndSim > 0 || (rx->simflg & 1) != 0 ))) {
      if (u_inis != NULL) {
        ind->isIni = 1;
        // Also can update individual random variables (if needed)
        // Use rxEffNeq() for the memcpy length: under a per-individual
        // neqOverride (predNeq < op->neq), the per-event buffer stride is
        // predNeq, so writing op->neq doubles into slot 0 would clobber the
        // start of slot 1 (which sits at offset predNeq, not op->neq).
        if (inLhs == 0) memcpy(ind->solve, op->inits, rxEffNeq(ind, op)*sizeof(double));
        u_inis(solveid, ind->solve); // Update initial conditions @ current time
        ind->isIni = 0;
      } else if (rx->nMtime && inLhs == 0 && op->neq > 0) {
        // No u_inis but state-dep mtime needs initial values in ind->solve
        memcpy(ind->solve, op->inits, rxEffNeq(ind, op)*sizeof(double));
      }
		}
    // Compute model times using ind->solve (which has user-specified inits after u_inis).
    // ind->solve is always a valid calloc'd pointer, unlike op->inits which may be unset.
    if (rx->nMtime) {
      if (inLhs == 0 || op->neq == 0) {
        // ODE solve pass (inLhs==0) or LHS-only model (neq==0): initialise mtime.
        // Compute mtime with actual initial state -> mtime_init[k].
        double *_initState = (inLhs == 0 && op->neq > 0) ? ind->solve : op->inits;
        if (ind->fns && ind->fns->mtime) ind->fns->mtime(solveid, ind->mtime, _initState);

        // Compute mtime with zero state -> base (state-independent) time mtime_base[k].
        // If base <= init, place event at base so the solver is forced to visit base,
        // then recomputeMtimeIfNeeded re-evaluates with actual state(base) and reschedules
        // to base + f(state(base)) >= base.  This is the correct semantics: state-dep offset
        // is evaluated at the trigger time, not at t=0.
        // If base > init (e.g. negative initial offset shifts event earlier), keep init
        // so the event fires at the correct earlier time (old behaviour preserved).
        double _baseMtime[90];
        if (op->neq > 0) {
          double *_zeroState = new double[op->neq]();  // zero-initialised
          if (ind->fns && ind->fns->mtime) ind->fns->mtime(solveid, _baseMtime, _zeroState);
          delete[] _zeroState;
        } else {
          if (ind->fns && ind->fns->mtime) ind->fns->mtime(solveid, _baseMtime, _initState);
        }
        std::fill_n(ind->mtime0, rx->nMtime, R_NegInf);
        for (int k = 0; k < rx->nMtime; k++) {
          if (_baseMtime[k] <= ind->mtime[k]) {
            // Event is at or after the base time: place at base so solver visits it.
            ind->mtime[k] = _baseMtime[k];
          }
          // else: event is before base (negative offset); keep mtime_init (old behaviour).
          ind->mtime0[k] = ind->mtime[k];  // trigger = initial placement
        }
      }
      // else: LHS pass (inLhs==1, neq>0) -- preserve ind->mtime[k] set by the ODE
      // solve (including any recomputeMtimeIfNeeded updates).  getTime_ returns
      // ind->mtime[evid-10] so using the correct final time is essential for the
      // output dataframe to show the actual event time, not the trigger time.
    }
		ind->_newind = 1;
		ind->dosenum = 0;
		ind->tlast = NA_REAL;
		ind->tfirst = NA_REAL;
		ind->curDose = NA_REAL;
		if (inLhs == 0 || (inLhs == 1 && op->neq==0)) {
			ind->solved = -1;
		}
    if (inLhs == 0 || op->neq == 0) {
      // Sort for solving (inLhs==0), or for LHS-only models (neq==0) where no
      // ODE solver ran.  When inLhs==1 and neq>0 the ODE solve loop already ran
      // sortInd and may have re-sorted for state-dep lag; preserve that order so
      // getSolve(i) positions in rxode2_df agree with the solve loop.
      sortInd(ind);
      // Note: op->badSolve is NOT checked here -- it is a shared global flag.
      // In parallel mode another thread's failed solve would prevent THIS
      // individual from initializing.  Bad-solve state is handled per-individual
      // via localBadSolve / *rc in the caller.
    }
    ind->mainSorted = 1;
		ind->ixds=ind->idx=0;
    if (ncmt) ind->pendingDosesN[0] = 0;
		return 1;
	}
  static inline void handleEvid3(rx_solving_options_ind *ind, rx_solving_options *op,
                                 rx_solve *rx, int *neq, double *xp, double *xout,
                                 double *yp, int *idid, t_update_inis u_inis) {
    ind->curShift -= rx->maxShift;
    for (unsigned int j = neq[0]; j--;) {
      ind->InfusionRate[j] = 0;
      ind->on[j] = 1;
      ind->cacheME=0;
    }
    cancelInfusionsThatHaveStarted(ind, neq[1], *xout);
    cancelPendingDoses(ind, neq[1]);
    memcpy(yp, op->inits, neq[0]*sizeof(double));
    u_inis(neq[1], yp); // Update initial conditions @ current time
    if (rx->istateReset) *idid = 1;
    *xout -= rx->maxShift;
    *xp = *xout;
    ind->linCmtAlast = yp ;
    ind->ixds++;
    // Reset dose-tracking after time reset so tad is never negative post-reset.
    // Any dose that fires after this EVID=3 re-establishes tlast from scratch.
    int _ncmt = op->neq + op->extraCmt;
    for (int _j = _ncmt; _j--;) {
      ind->tlastS[_j]   = NA_REAL;
      ind->curDoseS[_j] = NA_REAL;
    }
    ind->tlast   = NA_REAL;
    ind->curDose = NA_REAL;
  }

#if defined(__cplusplus)
}

static inline int handleExtraDose(int *neq,
                                  int *BadDose,
                                  double *InfusionRate,
                                  double *dose,
                                  double *yp,
                                  double xout, double xp, int id,
                                  int *i, int nx,
                                  int *istate,
                                  rx_solving_options *op,
                                  rx_solving_options_ind *ind,
                                  t_update_inis u_inis,
                                  void *ctx) {
  if (ind->extraDoseN[0] > ind->idxExtra) {
    if (ind->extraSorted == 0) {
      // do sort
      SORT(ind->extraDoseTimeIdx + ind->idxExtra, ind->extraDoseTimeIdx + ind->extraDoseN[0],
           [ind](int a, int b){
             double timea = ind->extraDoseTime[a],
               timeb = ind->extraDoseTime[b];
             if (timea == timeb) {
               int evida = ind->extraDoseEvid[a],
                 evidb = ind->extraDoseEvid[b];
               if (evida == evidb){
                 return a < b;
               }
               return evida < evidb;
             }
             return timea < timeb;
           });
      ind->extraSorted=1;
      ind->idxExtra=0;
    }
    // Use "real" xout for handle_evid functions.
    int idx = ind->idx;
    int ixds = ind->ixds;
    int trueIdx = ind->extraDoseTimeIdx[ind->idxExtra];
    ind->idx = -1-trueIdx;
    double time = getAllTimes(ind, ind->idx);
    while (!isSameTimeOp(time, xp) && time < xp && ind->idxExtra < ind->extraDoseN[0]) {
      ind->idxExtra++;
      trueIdx = ind->extraDoseTimeIdx[ind->idxExtra];
      ind->idx = -1-trueIdx;
      time = getAllTimes(ind, ind->idx);
    }
    if ((isSameTimeOp(time, xp) || time > xp) &&
        (isSameTimeOp(time, xout) || time <= xout)) {
      bool ignore = true;
      while (ignore && time <= xout) {
        ignore=false;
        for (int i = 0; i < ind->ignoredDosesN[0]; ++i) {
          int curIdx = ind->ignoredDoses[i];
          if (curIdx < 0 && -1-curIdx == trueIdx) {
            ignore = true;
            break;
          }
        }
        if (ignore) {
          ind->idxExtra++;
          if (ind->idxExtra < ind->extraDoseN[0]) {
            trueIdx = ind->extraDoseTimeIdx[ind->idxExtra];
            ind->idx = -1-trueIdx;
            time = getAllTimes(ind, ind->idx);
          } else {
            ind->idxExtra--;
            break;
          }
        } else {
          break;
        }
      }
      if (ignore) {
        ind->idx = idx;
        ind->ixds = ixds;
        return 0;
      } else {
        ind->extraDoseNewXout = time;
        ind->idx = idx;
        ind->ixds = ixds;
        // REprintf("time: %f; xp: %f; xout: %f; handleExtra\n", time, xp, xout);
        return 1;
      }
    }
    ind->idx = idx;
    ind->ixds = ixds;
    return 0;
  }
  return 0;
}

static inline void preSolve(rx_solving_options *op, rx_solving_options_ind *ind,
                            double &xp, double &xout, double *yp) {
  // Event ("jump") sensitivities: flush the deferred non-dosed dtau jump.  It is
  // recorded at a modeled-lag dose arrival time tau but withheld from yp so that
  // any output coincident with tau reports the pre-jump left limit for
  // compartments that did not receive the bolus.  This is the first integration
  // step that advances past tau (preSolve is only called when xout != xp), so
  // adding the jump here makes every t > tau see the post-jump value -- identical
  // to applying it at the dose event, since no integration occurred in between.
  // Invariant: a flush happens between any two distinct event times, so all
  // pending accumulation shares the single time esPendingTau.
  if (ind->esHasPending && ind->esPendingJump != NULL &&
      isSameTime(ind->esPendingTau, xp)) {
    int _n = rxEffNeq(ind, op);
    for (int j = _n; j--;) yp[j] += ind->esPendingJump[j];
    memset(ind->esPendingJump, 0, (size_t)_n*sizeof(double));
    ind->esHasPending = 0;
  }
  // First set the last values of time and compartment values
  if (op->numLin > 0) {
    ind->linCmtAlast = yp + op->linOffset;
    ind->tprior = xp + ind->curShift; // Set the time to the time to solve to.
    ind->tout   = xout + ind->curShift;
  }
  ind->_atEventTime = 1;
}


#endif

#endif
