/*
   This file is part of the RELXILL model code.

   RELXILL is free software: you can redistribute it and/or modify it
   under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   any later version.

   RELXILL is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
   GNU General Public License for more details.
   For a copy of the GNU General Public License see
   <http://www.gnu.org/licenses/>.

    Copyright 2022 Thomas Dauser, Remeis Observatory & ECAP
*/
#include "IonGradient.h"
#include "Rebin.h"
#include "Relbase.h"
#include <cstring>

extern "C"{
#include "writeOutfiles.h"
}

// calculate the log(xi) for given density and emissivity
// note: delta_inc is taking the correction into account that xillver
//       is calculated for a fixed delta_inc=PI/4 (see Ingram+19)
static double cal_lxi(double dens, double emis, double delta_inc) {
  double const lxi_corrfactor = cos(M_PI / 4) / cos(delta_inc);
  return log10(4.0 * M_PI * emis / dens * lxi_corrfactor);
}

/**
  * adjust "values" to be within the limits of the xillver table
  **/
void adjust_parameter_to_be_within_xillver_bounds(double *values, int nzones, double tab_min, double tab_max) {
  for (int ii = 0; ii < nzones; ii++) {
    if (values[ii] < tab_min) {
      values[ii] = tab_min;
    } else if (values[ii] > tab_max) {
      values[ii] = tab_max;
    }
  }
}

// we use the same definition as Adam with r_peak = (11/9)^2 rin to be consistent (does not matter much)
static double get_radius_lxi_max_ss73(double rin) {
  return pow((11. / 9.), 2) * rin;
}

/**
 * @brief calculate the emissivit profile (emissivity, del_inc, del_emit)for the give radius
 * from interpolation of the emissivity profile
 * @param rad            [input]
 * @param emis_profile   [input]
 */
static emisProfile *get_emis_at_rad(double rad, const emisProfile &emis_profile) {

  // we only need one bin of the emissivity profile
  const int nbins = 1;
  const int ibin = 0;

  int status = EXIT_SUCCESS;
  auto radial_grid = new double[nbins];
  radial_grid[ibin] = rad;
  auto emis_struct = new_emisProfile(radial_grid, nbins, &status);
  delete[] radial_grid;

  int kk;
  double interp;
  if (emis_profile.re[0] > emis_profile.re[1]) {
    // descending radial grid (standard AD grid)
    kk = inv_binary_search(emis_profile.re, emis_profile.nr, rad);
    interp = (rad - emis_profile.re[kk + 1]) / (emis_profile.re[kk] - emis_profile.re[kk + 1]);
    // descending: interp=0 -> kk+1 (small r), interp=1 -> kk (large r)
    emis_struct->emis[ibin] = interp_lin_1d(interp, emis_profile.emis[kk + 1], emis_profile.emis[kk]);
    emis_struct->del_inc[ibin] = interp_lin_1d(interp, emis_profile.del_inc[kk + 1], emis_profile.del_inc[kk]);
    emis_struct->del_emit[ibin] = interp_lin_1d(interp, emis_profile.del_emit[kk + 1], emis_profile.del_emit[kk]);
  } else {
    // ascending radial grid (return radiation grid)
    kk = binary_search(emis_profile.re, emis_profile.nr, rad);
    interp = (rad - emis_profile.re[kk]) / (emis_profile.re[kk + 1] - emis_profile.re[kk]);
    // ascending: interp=0 -> kk, interp=1 -> kk+1
    emis_struct->emis[ibin] = interp_lin_1d(interp, emis_profile.emis[kk], emis_profile.emis[kk + 1]);
    emis_struct->del_inc[ibin] = interp_lin_1d(interp, emis_profile.del_inc[kk], emis_profile.del_inc[kk + 1]);
    emis_struct->del_emit[ibin] = interp_lin_1d(interp, emis_profile.del_emit[kk], emis_profile.del_emit[kk + 1]);
  }

  assert(emis_struct->re[0] > 1.0);

  return emis_struct;
}

/**
 * @brief calculates the emissivity profile at the radius of maximal ionization [only 1 value]
 * @param rin (inner radius of the accretion disk)
 * @param emis_profile
 * @return
 */
static emisProfile *get_emis_profile_lxi_max(const double rin, const emisProfile &emis_profile) {
  const double radius_lxi_max = get_radius_lxi_max_ss73(rin);
  auto emis_profile_rad = get_emis_at_rad(radius_lxi_max, emis_profile);

  return emis_profile_rad;
}

/**
 * @brief: free an emissivity profile including the radial grid
 * @param emis_profile
 */
void free_emis_profile_rad(emisProfile *emis_profile) {
  free_emisProfile(emis_profile);
}

// determine the maximal ionization
static double cal_lxi_max_ss73(const emisProfile &emis_profile, double rin) {

  const double rad_lxi = get_radius_lxi_max_ss73(rin);
  auto emis_profile_rad = get_emis_profile_lxi_max(rin, emis_profile); // 1dim arrays


  const double density_lxi_max = density_ss73_zone_a_normalized(rad_lxi, rin);
  const double lxi_max = cal_lxi(density_lxi_max,
                                 emis_profile_rad->emis[0],
                                 emis_profile_rad->del_inc[0]);

  free_emis_profile_rad(emis_profile_rad);

  return lxi_max;
}

double *IonGradient::rebin_emis_to_zones(const emisProfile &emis_profile) {
  irradiating_flux = new double[m_nzones];
  auto del_inc = new double[m_nzones];
  setArrayToZero(irradiating_flux, m_nzones);
  setArrayToZero(del_inc, m_nzones);

  int status = EXIT_SUCCESS;
  assert(emis_profile.del_inc != nullptr);
  inv_rebin_mean(emis_profile.re, emis_profile.emis, emis_profile.nr, m_rmean, irradiating_flux, m_nzones, &status);
  inv_rebin_mean(emis_profile.re, emis_profile.del_inc, emis_profile.nr, m_rmean, del_inc, m_nzones, &status);

  if (status != EXIT_SUCCESS) {
    throw std::exception();
  }

  return del_inc;
}

void IonGradient::set_irradiating_flux_from_emis(const emisProfile &emis_profile) {
  assert(emis_profile.nr == m_nzones);
  assert(emis_profile.del_inc != nullptr);

  irradiating_flux = new double[m_nzones];
  for (int ii = 0; ii < m_nzones; ii++) {
    irradiating_flux[ii] = emis_profile.emis[ii];
  }
}

void IonGradient::calc_lxi_and_density(const double *del_inc, double fac_lxi_norm, double param_density) {
  const double rin = radial_grid.radius[0];

  /** calculate the density for a stress-free inner boundary condition, i.e., R0=rin in SS73) **/
  const double rad_density_min = (25. / 9.) * rin;
  const double density_min = density_ss73_zone_a(rad_density_min, rin);
  for (int ii = 0; ii < m_nzones; ii++) {
    double density_normed = 1.0;
    if (not constantDiskDensity()) {
      density_normed = density_ss73_zone_a(m_rmean[ii], rin) / density_min;
    }

    dens[ii] = log10(density_normed) + param_density;

    // now we can use the emissivity to calculate the ionization
    // (note we need a density which is normalized to dens(rin)=1 )
    lxi[ii] = cal_lxi(density_normed, irradiating_flux[ii], del_inc[ii]);
    lxi[ii] += fac_lxi_norm;
  }
}

static bool is_emis_on_zone_grid(const emisProfile &emis_profile, const double *rmean, int nzones) {
  if (emis_profile.nr != nzones) {
    return false;
  }
  const double prec = 1e-6;
  for (int ii = 0; ii < nzones; ii++) {
    if (fabs(emis_profile.re[ii] - rmean[ii]) / rmean[ii] > prec) {
      return false;
    }
  }
  return true;
}

/**
 * @brief: calculate the ionization gradient for the alpha type models
 * @param emis_profile
 * @param param_xlxi0
 * @param param_density
 */
void IonGradient::calc_ion_grad_alpha(const emisProfile &emis_profile, double param_xlxi0, double param_density) {
  double *del_inc;
  if (is_emis_on_zone_grid(emis_profile, m_rmean, m_nzones)) {
    set_irradiating_flux_from_emis(emis_profile);
    del_inc = new double[m_nzones];
    std::memcpy(del_inc, emis_profile.del_inc, m_nzones * sizeof(double));
  } else {
    del_inc = rebin_emis_to_zones(emis_profile);
  }

  const double rin = radial_grid.radius[0];
  // calculate the maximal ionization assuming r^-3 and SS73 alpha disk (see Ingram+19)
  const double lxi_max = cal_lxi_max_ss73(emis_profile, rin);

  // the maximal ionization is given as an input parameter, so we need to normalize our calculation by this value
  const double fac_lxi_norm = param_xlxi0 - lxi_max; // subtraction instead of division because of the log

  calc_lxi_and_density(del_inc, fac_lxi_norm, param_density);

  delete[] del_inc;
}


void IonGradient::calc_ion_grad_pl(double xlxi0, double xindex, double inputval_dens) {
  for (int ii = 0; ii < m_nzones; ii++) {
    lxi[ii] = (exp(xlxi0))
        * pow((m_rmean[ii] / m_rmean[0]), -1.0 * xindex);
    lxi[ii] = log(lxi[ii]);

    dens[ii] = inputval_dens;
  }
}

void IonGradient::calc_energy_shift_from_source_to_disk(const relParam *rel_param) const {
  assert (del_emit != nullptr);

  for (int ii = 0; ii < m_nzones; ii++) {
    if (is_any_primary_source(rel_param->emis_type)) {
      m_energy_shift_source_disk[ii] = energy_shift_source_disk(rel_param, m_rmean[ii], del_emit[ii]);
    } else {
      m_energy_shift_source_disk[ii] = 1.0;
    }
  }
}

xillTableParam **IonGradient::get_xill_param_zone(const xillTableParam *primary_source_xilltab_params) const {

  auto xill_param_zone = new xillTableParam *[m_nzones];

  for (int ii = 0; ii < m_nzones; ii++) {
    xill_param_zone[ii] = new xillTableParam;
    (*xill_param_zone[ii]) = (*primary_source_xilltab_params);  // shallow copy (enough for the parameter structure)

    // set xillver parameters for the given zone
    xill_param_zone[ii]->ect = primary_source_xilltab_params->ect * m_energy_shift_source_disk[ii];
    xill_param_zone[ii]->lxi = lxi[ii];
    xill_param_zone[ii]->dens = dens[ii];
  }

  return xill_param_zone;
}

double calc_flux_rin_cgs_lp(double l_source, double height, double Rin, double mass) {

  const double CONST_rgIncm = 1.4822e5;  // G / c^2 * (M/Msolar)

  const double dRay_rg = sqrt(pow(height, 2) + pow(Rin, 2));
  const double dRay_cm = dRay_rg * CONST_rgIncm * mass;

  return l_source / (4 * M_PI * pow(dRay_cm, 2));
}

/* calc_flux_rin_cgs_lp assumes a lp source, this one is for ring source */
double calc_flux_rin_cgs_ring(double l_source, double r_src, double theta, double Rin, double mass) {

  const double CONST_rgIncm = 1.4822e5;  // G / c^2 * (M/Msolar)

  const double dRay_rg = sqrt(pow(r_src, 2) + pow(Rin, 2) - 2.0 * Rin * r_src * sin(theta * CONVERT_DEG2RAD));
  const double dRay_cm = dRay_rg * CONST_rgIncm * mass;

  return l_source / (4 * M_PI * pow(dRay_cm, 2));
}

/**
 * @brief: for a given primary source configuration (mass, distance, observed flux), we calculate the flux at the
 * given radius on the disk, using no GR for the flux conversion from source to disk (i.e., no energy shifts and
 * and straight photons paths)
 * @param photon_fate_fractions
 * @param primary_source
 * @param rad
 * @return
 */
double calc_flux_disk_newton_cgs(const lpReflFrac *photon_fate_fractions,
                                 const PrimarySourceParameters &primary_source,
                                 double rad) {
  const auto &rel_param = (*primary_source.rel_param());

  // calculate the source luminosity for boost to get a higher/lower source luminosity
  // note: at add_primary_component this still needs to be added to the normalization of the reflection spectrum
  double l_source_cgs = primary_source.luminosity_source_cgs();
  const double boost = primary_source.get_boost_parameter(photon_fate_fractions);
  l_source_cgs *= boost;
  if (is_ring_primary_source(rel_param.emis_type) || is_slab_primary_source(rel_param.emis_type)) {
    // AN: for ring/slab sources the flux at rin is different, but as far as I am aware, this function is used only for
    // relxillAlpha which does not have extended geometry yet, so I leave a warning here. TODO proper calculation?
    printf("calc_flux_disk_newton_cgs may not work properly for extended sources! Check IonGradient.cpp \n");
    // Still try to make it work for rings and slabs
    double r_sph = rel_param.r_src;
    double theta = rel_param.theta;
    if (rel_param.r_src == 0.0) {
      // r_src, theta not defined for slab
      convert_hx_to_rtheta(rel_param.height, rel_param.x, rel_param.a, &r_sph, &theta);
    }
    return calc_flux_rin_cgs_ring(l_source_cgs, r_sph, theta, rad, primary_source.mass_msolar());
  } else {
    return calc_flux_rin_cgs_lp(l_source_cgs, rel_param.height, rad, primary_source.mass_msolar());
  }
}

/**
 * @brief calculate the flux on the disk at the radius given by the emissivity profile
 * (require the emissivity profile to be only of length=1!)
 * @param photon_fate_fractions
 * @param emis_profile_lxi_max  (is only allowed to contain one value)
 * @param primary_source
 * @return
 */
double calc_flux_disk_cgs(const lpReflFrac *photon_fate_fractions,
                          const emisProfile &emis_profile_lxi_max,
                          const PrimarySourceParameters &primary_source) {
  assert(emis_profile_lxi_max.nr == 1);

  const double radius_lxi_max = emis_profile_lxi_max.re[0];
  const auto &rel_param = (*primary_source.rel_param());

  // calculate the flux boost wrt to Newton from the given emissivity profile, as it is defined such that for large
  // radii (i.e., neglecting GR effects it coincides with the Newtonian version normalized as \int \emis dA = 1)

  double emis_at_lxi_max = calc_emissivity_newton(&rel_param, radius_lxi_max);

  double const boost_newton_to_gr =
      photon_fate_fractions->refl_frac * emis_profile_lxi_max.emis[0] / emis_at_lxi_max;

  const double flux_rad_lxi_max_cgs =
      calc_flux_disk_newton_cgs(photon_fate_fractions, primary_source, radius_lxi_max)
          * boost_newton_to_gr;

  return flux_rad_lxi_max_cgs;
}

/**
 * @brief: calculates the value of maximal value of log(xi) for an alpha disk density gradient for a given
 * density at the Rin (note that the maximal value of log(xi) is not at Rin, but (11/9)^2*Rin)
 * @param emis_profile
 * @param primary_source
 * @param log_density_rin
 * @return
 */
double IonGradient::calculate_lxi_max_from_distance(const emisProfile &emis_profile,
                                                    const PrimarySourceParameters &primary_source,
                                                    double log_density_rin) {

  const auto rel_param = primary_source.rel_param();

  auto emis_profile_lxi_max = get_emis_profile_lxi_max(rel_param->rin, emis_profile);
  assert(emis_profile_lxi_max->nr == 1);

  const double radius_lxi_max = emis_profile_lxi_max->re[0];

  const double flux_rad_lxi_max_cgs =
      calc_flux_disk_cgs(emis_profile.photon_fate_fractions, (*emis_profile_lxi_max), primary_source);

  // the routine "density_ss73_zone_a" returns a density of 1 at rin
  const double density_lxi_max = density_ss73_zone_a(radius_lxi_max, rel_param->rin) * pow(10, log_density_rin);

  const double lxi_max = cal_lxi(density_lxi_max, flux_rad_lxi_max_cgs, emis_profile_lxi_max->del_inc[0]);

  free_emis_profile_rad(emis_profile_lxi_max);

  return lxi_max;
}

/**
 * @brief calculates the physical emissivity (meaning in units of erg/cm2/s) for the given source configuration
 * needs the irradiating_flux to be calculated
 * @param primary_source
 * @param emis_profile
 */
void IonGradient::calculate_physical_emissivity(const PrimarySourceParameters &primary_source,
                                                const emisProfile &emis_profile) {

  if (irradiating_flux == nullptr) {
    return;
  }

  const auto &rel_param = (*primary_source.rel_param());

  auto emis_profile_lxi_max = get_emis_profile_lxi_max(rel_param.rin, emis_profile);

  const double flux_rad_lxi_max_cgs =
      calc_flux_disk_cgs(emis_profile.photon_fate_fractions, (*emis_profile_lxi_max), primary_source);

  const double physical_emis_normalization_factor = flux_rad_lxi_max_cgs / emis_profile_lxi_max->emis[0];

  free_emis_profile_rad(emis_profile_lxi_max);

  m_physical_irrad_flux = new double[m_nzones];
  for (int ii = 0; ii < m_nzones; ii++) {
    m_physical_irrad_flux[ii] = irradiating_flux[ii] * physical_emis_normalization_factor;
  }

}


void IonGradient::calculate_gradient(const emisProfile &emis_profile,
                                     const PrimarySourceParameters &primary_source_params) {

  // TODO proper angle calculation for extended sources (need extra raytracing to save angles)
  set_del_emit_for_each_zone(emis_profile);

  auto rel_param = primary_source_params.rel_param();
  auto xill_param = primary_source_params.xilltab_param();
  //assert(rel_param != nullptr);
 // assert(xill_param != nullptr);

  calc_energy_shift_from_source_to_disk(rel_param);

  if (m_ion_grad_type == ION_GRAD_TYPE_PL) {
    calc_ion_grad_pl(xill_param->lxi, m_ion_grad_index, xill_param->dens);

  } else if (m_ion_grad_type == ION_GRAD_TYPE_ALPHA) {

    const double lxi_max = (is_alpha_model(rel_param->model_type))
                           ? IonGradient::calculate_lxi_max_from_distance(emis_profile,
                                                                          primary_source_params,
                                                                          xill_param->dens)
                           : xill_param->lxi;

    calc_ion_grad_alpha(emis_profile, lxi_max, xill_param->dens);

  } else if (m_ion_grad_type == ION_GRAD_TYPE_CONST) {
    for (int ii = 0; ii < m_nzones; ii++) {
      lxi[ii] = xill_param->lxi;
      dens[ii] = xill_param->dens;
    }

  } else {
    printf(" *** ionization type with number %i not implemented \n", m_ion_grad_type);
    printf("     choose either %i for the PL, %i for the ALPHA-disk, or %i for constant\n",
           ION_GRAD_TYPE_PL, ION_GRAD_TYPE_ALPHA, ION_GRAD_TYPE_CONST);
    throw std::exception();
  }

  if (is_alpha_model(rel_param->model_type)) {
    calculate_physical_emissivity(primary_source_params, emis_profile);
  }

  if (shouldOutfilesBeWritten()) {
    //  if (is_alpha_model(rel_param->model_type)) {
    //    calculate_physical_emissivity(primary_source_params, emis_profile);
    //  }
    IonGradient::write_to_file("__relxillOutput_iongrad.dat");
  }

  // need to make sure lxi and logn are within the xillver table values //  TODO: Need to define this automatically
  adjust_parameter_to_be_within_xillver_bounds(lxi, m_nzones, 0.0, 4.7);

}

/**
 * @brief: write the ionization gradient to file; for the alpha type gradient, als the irradiating flux
 * is written
 * @param fout
 */
void IonGradient::write_to_file(const char *fout) const {

  FILE *fp = fopen(fout, "w+");

  if (irradiating_flux != nullptr) {
    fprintf(fp, "# rlo [R_g]\t rhi [R_g] \t log(xi) \t log(N) \t delta_emit \n F_x\n");

    // if we calculated the physical flux, use this for the output
    const double *irrad_flux = (m_physical_irrad_flux != nullptr) ? m_physical_irrad_flux : irradiating_flux;

    for (int ii = 0; ii < m_nzones; ii++) {
      fprintf(fp, " %e \t %e \t %e \t %e \t %e \t %e \n", radial_grid.radius[ii], radial_grid.radius[ii + 1],
              lxi[ii], dens[ii], del_emit[ii], irrad_flux[ii]);
    }
  } else {
    fprintf(fp, "# rlo [R_g]\t rhi [R_g] \t log(xi) \t log(N) \t delta_emit \n ");
    for (int ii = 0; ii < m_nzones; ii++) {
      fprintf(fp, " %e \t %e \t %e \t %e \t %e  \n", radial_grid.radius[ii], radial_grid.radius[ii + 1],
              lxi[ii], dens[ii], del_emit[ii]);
    }
  }
  fclose_errormsg(fp, fout);
}

void IonGradient::set_del_emit_for_each_zone(const emisProfile &emis_profile) {
  if (del_emit == nullptr) {
    del_emit = new double[m_nzones];
  }
  if (is_emis_on_zone_grid(emis_profile, m_rmean, m_nzones)) {
    std::memcpy(del_emit, emis_profile.del_emit, m_nzones * sizeof(double));
  } else {
    int status = EXIT_SUCCESS;
    inv_rebin_mean(emis_profile.re, emis_profile.del_emit, emis_profile.nr, m_rmean, del_emit, m_nzones, &status);
    if (status != EXIT_SUCCESS) {
      throw std::exception();
    }
  }
}

/** @brief calculate the Ecut/kTe value as seen by the disk zone (if nzones>1)
 *        - for nzones=1 the value at the primary source is returned
 *        - the cutoff is calculated for the (linear) middle of the radial zone
 * @param rel_param
 * @param ecut_primary [ecut/kTe value at the primary source]
 * @param izone [zone index]
 * @return
 */
double IonGradient::get_ecut_disk_zone(const relParam *rel_param, double ecut_primary, int izone) const {

  if (radial_grid.num_zones() == 1) {
    return ecut_primary; // TODO: not obvious what "ecut0" is (it is the input value, from the model fitting)
  } else {
    double const rzone = 0.5 * (radial_grid.radius[izone] + radial_grid.radius[izone + 1]);

    if (del_emit == nullptr) { // del_emit relevant if beta!=0 (doppler boosting)
      printf(" *** error in IonGradient: ionization gradient not calculated \n");
      printf("       can not calculate Ecut/kTe on the disk \n\n");
      throw std::exception();
    }
    if (is_ring_primary_source(rel_param->emis_type) || is_slab_primary_source(rel_param->emis_type)) {
      return ecut_primary * gi_potential_ext(rel_param, rzone);
    }
    else {
      return ecut_primary * gi_potential_lp(rzone, rel_param->a, rel_param->height, rel_param->beta, del_emit[izone]);
    }
  }
}

RadialGrid get_standard_relxill_radial_grid(const relParam& rel_param) {

  // transition radius is chosen based on the radius where emissivity converges to classical r^-3 dpendence
  double transition_radius;
  if (is_ring_primary_source(rel_param.emis_type)) {
    transition_radius = rel_param.r_src;
  } else if (is_slab_primary_source(rel_param.emis_type)) {
    transition_radius = sqrt(rel_param.height * rel_param.height + rel_param.x * rel_param.x);
  } else {
    transition_radius = rel_param.height;
  }

  // radial grid assumes log-spacing for r<transition_radius and 1/r spacing for r>transition_radius
  return {rel_param.rin, rel_param.rout, rel_param.num_zones, transition_radius};
}


/**
 * @brief Calculate a radial grid for disk zones with adaptive spacing
 * The spacing is defined by the requirement: comparable flux in each radial zone
 * Spacing 1/r is good for outer disk, where F ~ r^-3, otherwise log(r)
 *
 * Creates a radial grid with nzones+1 boundary points between rmin and rmax.
 * The grid spacing adapts based on the transition radius parameter:
 * - For radii r < r_transition (when r_transition > rmin): Uses logarithmic spacing
 *   to better resolve the inner disk region where the emissivity varies rapidly
 * - For radii r >= r_transition: Uses 1/r spacing which provides finer resolution at
 *   smaller radii while extending efficiently to large radii
 * - Special case nzones=1: Returns simple [rmin, rmax] boundaries
 *
 * @param rmin Minimum radius of the grid (inner disk edge) [in gravitational radii]
 * @param rmax Maximum radius of the grid (outer disk edge) [in gravitational radii]
 * @param nzones Number of radial zones (grid will have nzones+1 boundary points)
 * @param r_transition_input Transition radius parameter, the height of the
 *                           primary source [in gravitational radii] for the Lamp post
 *                           spherical radius of the ring source
 *                           or spherical radius of the outer edge of the slab
 *
 * @return Vector of size nzones+1 containing the radial grid boundaries in ascending order
 *
 * @note The returned vector contains zone boundaries. For nzones zones, there are
 *       nzones+1 boundaries: [r_0, r_1, ..., r_nzones] where r_0=rmin and r_nzones=rmax
 * @note The return type is VecD, which is a vector of doubles
 */
VecD RadialGrid::calculate_radial_grid(double rmin, double rmax, const int nzones, double r_transition_input) {

  VecD rgrid(nzones + 1);

  if (nzones == 1) {
    rgrid[0] = rmin;
    rgrid[1] = rmax;
  } else {

    double r_transition = rmin;
    int indr = 0;

    if (r_transition_input > rmin) {
      r_transition = r_transition_input;

      get_log_grid(rgrid.data(), nzones + 1, rmin, rmax);
      indr = binary_search(rgrid.data(), nzones + 1, r_transition);

      r_transition = rgrid[indr];
    }

    if (indr < nzones) {
      double const rlo = r_transition;
      double const rhi = rmax; // radius[nzones];
      for (int ii = indr; ii < nzones + 1; ii++) {
        rgrid[ii] = 1.0 * (ii - indr) / (nzones - indr) * (1.0 / rhi - 1.0 / rlo) + 1.0 / rlo;
        rgrid[ii] = fabs(1.0 / rgrid[ii]);
      }

    }

  }

  return rgrid;
}
