/*******************************************************************************
 * This file is part of SWIFT.
 * Copyright (c) 2019 Loic Hausammann (loic.hausammann@epfl.ch)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 ******************************************************************************/

/* local headers */
#include "initial_mass_function.h"

#include "exp10.h"
#include "hdf5_functions.h"
#include "stellar_evolution_struct.h"

/**
 * @brief Get the IMF exponent in between mass_min and mass_max.
 */
float initial_mass_function_get_exponent(
    const struct initial_mass_function *imf, float mass_min, float mass_max) {

#ifdef SWIFT_DEBUG_CHECKS
  if (mass_max > imf->mass_max)
    error("Cannot have mass larger than the largest one in the IMF");

  /* 18.05.2024: This check is ill-defined. It needs to be improved.
     For population II stars, no problem.
     For population III stars, the snia->mass_min_progenitor can be smaller
     than the minimal mass of the IMF, which causes the code to crash here. */
  /* if (mass_min < imf->mass_min) */
  /*   error("Cannot have mass smaller than the smallest one in the IMF"); */
  if (mass_max < mass_min) error("Cannot have mass_min larger than mass_max");
#endif

  for (int i = 0; i < imf->n_parts; i++) {

    /* Check if in the correct part of the IMF */
    if (mass_min < imf->mass_limits[i + 1]) {

      /* Check if in only one segment */
      if (mass_max > imf->mass_limits[i + 1]) {
        error("Cannot get a single exponent for the interval [%g, %g]",
              mass_min, mass_max);
      }

      return imf->exp[i];
    }
  }

  error("Masses outside IMF ranges");

  return -1;
}

/** @brief Print the initial mass function */
void initial_mass_function_print(const struct initial_mass_function *imf) {

  if (engine_rank != 0) {
    return;
  }

  message("Number of parts: %i", imf->n_parts);
  message("Number of stars per mass units: %g", imf->N_tot);
  message("Mass interval: [%g, %g]", imf->mass_min, imf->mass_max);
  for (int i = 0; i < imf->n_parts; i++) {
    message("[%7.3f, %7.3f]: %5.2g * m^{%g}", imf->mass_limits[i],
            imf->mass_limits[i + 1], imf->coef[i], imf->exp[i]);
  }

  message("Mass fractions");
  for (int i = 0; i < imf->n_parts + 1; i++)
    message("m=%7.3f: x=%5.3f", imf->mass_limits[i], imf->mass_fraction[i]);
}

/** @brief Sample the initial mass function */
float initial_mass_function_sample(const struct initial_mass_function *imf,
                                   float f) {

  for (int i = 0; i < imf->n_parts; i++)
    if (f < imf->mass_fraction[i + 1]) {
      float pmin = pow(imf->mass_limits[i], imf->exp[i]);
      float exponent = 1. / imf->exp[i];
      float base_part_1 = imf->N_tot * imf->exp[i] / imf->coef[i];
      base_part_1 *= (f - imf->mass_fraction[i]);
      float base = base_part_1 + pmin;

      /* The mathematical expression is:
           (N_tot * exp_{imf, i} / coeff_{imf, i} * (f - mass_fraction_{imf, i})
             + pmin)**(1/exp_{imf, i})
      */
      return pow(base, exponent);
    }

  return -1;
}

/**
 * @brief Integrate the #interpolation_1d data with the initial mass function.
 *
 * The x are supposed to be linear in log.
 *
 * @param imf The #initial_mass_function.
 * @param data The data to integrate.
 * @param count The number of element in data.
 * @param log_mass_min The value of the first element.
 * @param step_size The distance between two points.
 */
void initial_mass_function_integrate(const struct initial_mass_function *imf,
                                     float *data, size_t count,
                                     float log_mass_min, float step_size) {

  /* Index in the data */
  size_t j = 1;
  const float mass_min = exp10(log_mass_min);
  const float mass_max = exp10(log_mass_min + (count - 1) * step_size);

  float m = mass_min;

  float *tmp = (float *)malloc(sizeof(float) * count);

  /* Set lower limit */
  tmp[0] = 0;
  for (int i = 0; i < imf->n_parts; i++) {

    /* Check if already in the correct part */
    if (mass_min > imf->mass_limits[i + 1]) {
      continue;
    }

    /* Check if already above the maximal mass */
    if (mass_max < imf->mass_limits[i]) {
      break;
    }

    /* Integrate the data */
    while ((m < imf->mass_limits[i + 1] || i == imf->n_parts - 1) &&
           j < count) {

      /* Compute the masses */
      const float log_m1 = log_mass_min + (j - 1) * step_size;
      const float m1 = exp10(log_m1);
      const float log_m2 = log_mass_min + j * step_size;
      float m2 = exp10(log_m2);

      /* Ensure that we stay within the limits */
      if (m2 > imf->mass_max) {
        m2 = imf->mass_max;
      }

      const float dm = m2 - m1;
      const float imf_1 = imf->coef[i] * pow(m1, imf->exp[i]);

      /* Get the imf of the upper limit  */
      float imf_2;
      if (m2 > imf->mass_limits[i + 1]) {
        imf_2 = imf->coef[i + 1] * pow(m2, imf->exp[i + 1]);
      } else {
        imf_2 = imf->coef[i] * pow(m2, imf->exp[i]);
      }

      /* Compute the integral */
      tmp[j] = tmp[j - 1] + 0.5 * (imf_1 * data[j - 1] + imf_2 * data[j]) * dm;

      /* Update j and m */
      j += 1;
      m = m2;
    }
  }

  /* The rest is extrapolated with 0 */
  for (size_t k = j; k < count; k++) {
    tmp[k] = tmp[k - 1];
  }

  /* Copy temporary array */
  memcpy(data, tmp, count * sizeof(float));

  /* clean everything */
  free(tmp);
}

/**
 * @brief Get the IMF coefficient in between mass_min and mass_max.
 *
 * @param imf The #initial_mass_function.
 * @param mass_min The minimal mass of the requested interval.
 * @param mass_max The maximal mass of the requested interval.
 *
 * @return The imf's coefficient of the interval.
 */
float initial_mass_function_get_coefficient(
    const struct initial_mass_function *imf, float mass_min, float mass_max) {

  for (int i = 0; i < imf->n_parts; i++) {

    /* Check if in the correct part of the IMF */
    if (mass_min < imf->mass_limits[i + 1]) {

      /* Check if in only one segment */
      if (mass_max > imf->mass_limits[i + 1]) {
        error("Cannot get a single coefficient for the interval [%g, %g]",
              mass_min, mass_max);
      }

      return imf->coef[i];
    }
  }

  error("Masses outside IMF ranges");

  return -1;
}

/**
 * @brief Compute the integral of the fraction number of the initial mass
 * function.
 *
 * @param imf The #initial_mass_function.
 * @param m1 The lower mass to evaluate.
 * @param m2 The upper mass to evaluate.
 *
 * @return The number fraction.
 */
float initial_mass_function_get_integral_xi(
    const struct initial_mass_function *imf, float m1, float m2) {

  /* Ensure the masses to be withing the limits */
  m1 = min(m1, imf->mass_max);
  m1 = max(m1, imf->mass_min);

  m2 = min(m2, imf->mass_max);
  m2 = max(m2, imf->mass_min);

  int k = -1;
  /* Find the correct part */
  for (int i = 0; i < imf->n_parts; i++) {
    if (m1 <= imf->mass_limits[i + 1]) {
      k = i;
      break;
    }
  }

  /* Check if found a part */
  if (k == -1) {
    error("Failed to find the correct function part: %g %g", m1, m2);
  }

  /* Check if m2 is inside the part */
  if (m2 < imf->mass_limits[k] || m2 > imf->mass_limits[k + 1]) {
    error("This function is not able to integrate in two different parts %g %g",
          m1, m2);
  }

  /* Compute the integral */
  const float int_xi1 = pow(m1, imf->exp[k]);
  const float int_xi2 = pow(m2, imf->exp[k]);

  return imf->coef[k] * (int_xi2 - int_xi1) / imf->exp[k];
};

/**
 * @brief Compute the mass fraction of the initial mass function.
 *
 * @param imf The #initial_mass_function.
 * @param m The mass to evaluate.
 *
 * @return The mass fraction.
 */
float initial_mass_function_get_imf(const struct initial_mass_function *imf,
                                    float m) {

  /* Check the mass to be within the limits */

  if (m > imf->mass_max || m < imf->mass_min)
    error("Mass below or above limits expecting %g < %g < %g.", imf->mass_min,
          m, imf->mass_max);

  for (int i = 0; i < imf->n_parts; i++) {
    if (m <= imf->mass_limits[i + 1]) {
      return imf->coef[i] * pow(m, imf->exp[i]);
    }
  }

  error("Failed to find correct function part: %g larger than mass max %g.", m,
        imf->mass_max);
  return 0.;
};

/**
 * @brief Compute the the mass fraction (of stars) between m1 and m2 per mass
 * unit.
 *
 * @param imf The #initial_mass_function.
 * @param m1 The lower mass to evaluate.
 * @param m2 The upper mass to evaluate.
 *
 * @return The integral of the mass fraction.
 */
float initial_mass_function_get_imf_mass_fraction(
    const struct initial_mass_function *imf, float m1, float m2) {

  /* Check that m2 is > m1 */
  if (m1 > m2)
    error("Mass m1 (=%g) larger or equal to m2 (=%g). This is not allowed", m1,
          m2);

  /* Check the masses to be within the limits */

  if (m1 > imf->mass_max || m1 < imf->mass_min)
    error("Mass m1 below or above limits expecting %g < %g < %g.",
          imf->mass_min, m1, imf->mass_max);

  if (m2 > imf->mass_max || m2 < imf->mass_min)
    error("Mass m2 below or above limits expecting %g < %g < %g.",
          imf->mass_min, m2, imf->mass_max);

  const int n = imf->n_parts;
  float integral = 0;

  /* loop over all segments */
  for (int i = 0; i < n; i++) {
    float mmin = max(imf->mass_limits[i], m1);
    float mmax = min(imf->mass_limits[i + 1], m2);

    if (mmin < mmax) {
      float p = imf->exp[i] + 1;
      integral += (imf->coef[i] / p) * (pow(mmax, p) - pow(mmin, p));
    } else /* nothing in this segment go to the next one */
      continue;

    if (m2 == mmax) /* nothing after this segment, stop */
      break;
  }

  return integral;
};

/**
 * @brief Compute the number fraction (of stars) between m1 and m2 per mass
 * unit.
 *
 * @param imf The #initial_mass_function.
 * @param m1 The lower mass to evaluate.
 * @param m2 The upper mass to evaluate.
 *
 * @return The integral of the mass fraction.
 */
float initial_mass_function_get_imf_number_fraction(
    const struct initial_mass_function *imf, float m1, float m2) {

  /* Check that m2 is > m1 */
  if (m1 > m2)
    error("Mass m1 (=%g) larger or equal to m2 (=%g). This is not allowed", m1,
          m2);

  /* Check the masses to be within the limits */

  if (m1 > imf->mass_max || m1 < imf->mass_min)
    error("Mass m1 below or above limits expecting %g < %g < %g.",
          imf->mass_min, m1, imf->mass_max);

  if (m2 > imf->mass_max || m2 < imf->mass_min)
    error("Mass m2 below or above limits expecting %g < %g < %g.",
          imf->mass_min, m2, imf->mass_max);

  const int n = imf->n_parts;
  float integral = 0;

  /* loop over all segments */
  for (int i = 0; i < n; i++) {
    float mmin = max(imf->mass_limits[i], m1);
    float mmax = min(imf->mass_limits[i + 1], m2);

    if (mmin < mmax) {
      float p = imf->exp[i];
      integral += (imf->coef[i] / p) * (pow(mmax, p) - pow(mmin, p));
    } else /* nothing in this segment go to the next one */
      continue;

    if (m2 == mmax) /* nothing after this segment, stop */
      break;
  }

  return integral;
};

/**
 * @brief Compute the coefficients of the initial mass function
 * as well as the mass fraction at the interface between IMF segments.
 *
 * @param imf The #initial_mass_function.
 */
void initial_mass_function_compute_coefficients(
    struct initial_mass_function *imf) {

  /* Allocate memory */
  if ((imf->coef = (float *)malloc(sizeof(float) * imf->n_parts)) == NULL)
    error("Failed to allocate the IMF coefficients.");

  /* Suppose that the first coefficients is 1 (will be corrected later) */
  imf->coef[0] = 1.;

  /* Use the criterion of continuity for the IMF */
  for (int i = 1; i < imf->n_parts; i++) {
    float exp = imf->exp[i - 1] - imf->exp[i];
    imf->coef[i] = imf->coef[i - 1] * pow(imf->mass_limits[i], exp);
  }

  /* Use the criterion on the integral = 1 */
  float integral = 0;
  for (int i = 0; i < imf->n_parts; i++) {
    const float exp = imf->exp[i] + 1.;
    const float m_i = pow(imf->mass_limits[i], exp);
    const float m_i1 = pow(imf->mass_limits[i + 1], exp);
    integral += imf->coef[i] * (m_i1 - m_i) / exp;
  }

  /* Normalize the coefficients (fix initial supposition) */
  for (int i = 0; i < imf->n_parts; i++) {
    imf->coef[i] /= integral;
  }

  /* Compute the total number of stars per mass unit */
  imf->N_tot = initial_mass_function_get_imf_number_fraction(imf, imf->mass_min,
                                                             imf->mass_max);

  /* Allocate the memory for the mass fraction */
  if ((imf->mass_fraction =
           (float *)malloc(sizeof(float) * (imf->n_parts + 1))) == NULL)
    error("Failed to allocate the IMF mass_fraction.");

  for (int i = 0; i < imf->n_parts + 1; i++) {
    imf->mass_fraction[i] = initial_mass_function_get_imf_number_fraction(
                                imf, imf->mass_min, imf->mass_limits[i]) /
                            imf->N_tot;
  }
}

/**
 * @brief Reads the initial mass function parameters from the tables.
 *
 * @param imf The #initial_mass_function.
 * @param params The #swift_params.
 * @param filename The filename of the chemistry table.
 */
void initial_mass_function_read_from_table(struct initial_mass_function *imf,
                                           struct swift_params *params,
                                           const char *filename) {

  hid_t file_id, group_id;

  /* Open IMF group */
  h5_open_group(filename, "Data/IMF", &file_id, &group_id);

  /* Read number of parts */
  io_read_attribute(group_id, "n", INT, &imf->n_parts);

  /* The tables have a different definition of n */
  imf->n_parts += 1;

  /* Allocate the memory for the exponents */
  if ((imf->exp = (float *)malloc(sizeof(float) * imf->n_parts)) == NULL)
    error("Failed to allocate the IMF exponents.");

  /* Read the exponents */
  io_read_array_attribute(group_id, "as", FLOAT, imf->exp, imf->n_parts);

  /* Allocate the memory for the temporary mass limits */
  if ((imf->mass_limits =
           (float *)malloc(sizeof(float) * (imf->n_parts + 1))) == NULL)
    error("Failed to allocate the IMF masses.");

  /* Read the mass limits */
  io_read_array_attribute(group_id, "ms", FLOAT, imf->mass_limits,
                          imf->n_parts - 1);

  /* Copy the data (need to shift for mass_min) */
  for (int i = imf->n_parts - 1; i > 0; i--) {
    imf->mass_limits[i] = imf->mass_limits[i - 1];
  }

  /* Read the minimal mass limit */
  io_read_attribute(group_id, "Mmin", FLOAT, &imf->mass_limits[0]);

  /* Read the maximal mass limit */
  io_read_attribute(group_id, "Mmax", FLOAT, &imf->mass_limits[imf->n_parts]);

  /* Close everything */
  h5_close_group(file_id, group_id);
}

/**
 * @brief Initialize the initial mass function.
 *
 * @param imf The #initial_mass_function.
 * @param phys_const The #phys_const.
 * @param us The #unit_system.
 * @param params The #swift_params.
 * @param filename The filename of the chemistry table.
 */
void initial_mass_function_init(struct initial_mass_function *imf,
                                const struct phys_const *phys_const,
                                const struct unit_system *us,
                                struct swift_params *params,
                                const char *filename) {

  /* Read the parameters from the yields table */
  initial_mass_function_read_from_table(imf, params, filename);

  /* Write the masses in the correct attributes */
  imf->mass_min = imf->mass_limits[0];
  imf->mass_max = imf->mass_limits[imf->n_parts];

  /* Compute the coefficients */
  initial_mass_function_compute_coefficients(imf);

  /* Print info */
  initial_mass_function_print(imf);
}

/**
 * @brief Zero pointers in initial_mass_function structs
 *
 * @param imf initial_mass_function struct in which pointers to tables
 * set to NULL
 */
void initial_mass_function_zero_pointers(struct initial_mass_function *imf) {

  imf->mass_limits = NULL;
  imf->mass_fraction = NULL;
  imf->exp = NULL;
  imf->coef = NULL;
}

/**
 * @brief Write a initial_mass_function struct to the given FILE as a stream of
 * bytes.
 *
 * Here we are only writing the arrays, everything else has been copied in the
 * feedback.
 *
 * @param imf the struct
 * @param stream the file stream
 * @param sm The #stellar_model.
 */
void initial_mass_function_dump(const struct initial_mass_function *imf,
                                FILE *stream, const struct stellar_model *sm) {

  /* Dump the mass limits. */
  if (imf->mass_limits != NULL) {
    restart_write_blocks((void *)imf->mass_limits, sizeof(float),
                         imf->n_parts + 1, stream, "imf_mass_limits",
                         "imf_mass_limits");
  }

  /*! Dump the exponents. */
  if (imf->exp != NULL) {
    restart_write_blocks((void *)imf->exp, sizeof(float), imf->n_parts, stream,
                         "imf_exponents", "imf_exponents");
  }

  /*! Dump the coefficients. */
  if (imf->coef != NULL) {
    restart_write_blocks((void *)imf->coef, sizeof(float), imf->n_parts, stream,
                         "imf_coef", "imf_coef");
  }

  /* Dump the mass fraction */
  if (imf->mass_fraction != NULL) {
    restart_write_blocks((void *)imf->mass_fraction, sizeof(float),
                         imf->n_parts + 1, stream, "imf_mass_fraction",
                         "imf_mass_fraction");
  }
}

/**
 * @brief Restore a initial_mass_function struct from the given FILE as a stream
 * of bytes.
 *
 * Here we are only writing the arrays, everything else has been copied in the
 * feedback.
 *
 * @param imf the struct
 * @param stream the file stream
 * @param sm The #stellar_model.
 */
void initial_mass_function_restore(struct initial_mass_function *imf,
                                   FILE *stream,
                                   const struct stellar_model *sm) {

  if (imf->n_parts > 0) {

    /* Restore the mass limits */
    imf->mass_limits = NULL;
    imf->mass_limits = (float *)malloc(sizeof(float) * (imf->n_parts + 1));
    if (imf->mass_limits == NULL) {
      error("Restart: Failed to allocate the IMF mass limits.");
    }
    restart_read_blocks((void *)imf->mass_limits, sizeof(float),
                        imf->n_parts + 1, stream, NULL, "imf_mass_limits");

    /* Restore the exponents */
    imf->exp = NULL;
    imf->exp = (float *)malloc(sizeof(float) * imf->n_parts);
    if (imf->exp == NULL) {
      error("Restart: Failed to allocate the IMF exponents.");
    }
    restart_read_blocks((void *)imf->exp, sizeof(float), imf->n_parts, stream,
                        NULL, "imf_exponents");

    /* Restore the coefficients */
    imf->coef = NULL;
    imf->coef = (float *)malloc(sizeof(float) * imf->n_parts);
    if (imf->coef == NULL) {
      error("Restart: Failed to allocate the IMF coefficients.");
    }
    restart_read_blocks((void *)imf->coef, sizeof(float), imf->n_parts, stream,
                        NULL, "imf_coef");

    /* Restore the mass fraction */
    imf->mass_fraction = NULL;
    imf->mass_fraction = (float *)malloc(sizeof(float) * (imf->n_parts + 1));
    if (imf->mass_fraction == NULL) {
      error("Restart: Failed to allocate the IMF mass fractions.");
    }
    restart_read_blocks((void *)imf->mass_fraction, sizeof(float),
                        imf->n_parts + 1, stream, NULL, "imf_mass_fraction");
  } else {
    error("Cannot allocate memory if imf->n_parts <= 0 (%d)!", imf->n_parts);
  }
}

/**
 * @brief Clean the allocated memory.
 *
 * @param imf the #initial_mass_function.
 */
void initial_mass_function_clean(struct initial_mass_function *imf) {

  /* Free the pointers */
  free(imf->mass_limits);
  imf->mass_limits = NULL;

  free(imf->exp);
  imf->exp = NULL;

  free(imf->coef);
  imf->coef = NULL;

  free(imf->mass_fraction);
  imf->mass_fraction = NULL;
}

/** @brief Sample a power law distribution (IMF)
 *
 * @param min_mass : the minimal IMF mass.
 * @param max_mass : the maximal IMF mass.
 * @param exp : the power law slope.
 * @param x : a random number in the range [0, 1].
 */
INLINE double initial_mass_function_sample_power_law(double min_mass,
                                                     double max_mass,
                                                     double exp, double x) {

  double pmin = pow(min_mass, exp);
  double pmax = pow(max_mass, exp);
  return pow(x * (pmax - pmin) + pmin, 1. / exp);
}

/** @brief Compute the mass of the continuous and discrete part of the
 * IMF. Also compute the total mass of the IMF.
 *
 * This function is used when we deal with an IMF split into two parts,
 * e.g. with the sink particles or the stellar feedback.
 *
 * Note: This function does not verify wheter it computes the masses for the
 * first stars or not. You need to verify this before this function and pass the
 * correct values to 'minimal_discrete_mass_Msun' and 'stellar_particle_mass'.
 *
 * Note 2: This function implicitly assumes M_sun since the IMF data
 * structures handles the masses in M_sun.
 *
 * @param imf The #initial_mass_function.
 * @param (return) M_continuous Mass of the continous part of the IMF.
 * @param (return) M_discrete Mass of the discrete part of the IMF.
 * @param (return) M_tot Total mass of the IMF.
 */
void initial_mass_function_compute_Mc_Md_Mtot(
    const struct initial_mass_function *imf, double *M_continuous,
    double *M_discrete, double *M_tot) {

  /* f_continuous is the imf mass fraction of the continuous part (of the IMF).
   */
  const double f_continuous = initial_mass_function_get_imf_mass_fraction(
      imf, imf->mass_min, imf->minimal_discrete_mass_Msun);

  /* Determine Mc and Md the masses of the continuous and discrete parts of the
     IMF, as well as Mtot the total mass of the IMF. */
  if (f_continuous > 0) {
    *M_tot = imf->stellar_particle_mass_Msun / f_continuous;
    *M_discrete = *M_tot - imf->stellar_particle_mass_Msun;
    *M_continuous = imf->stellar_particle_mass_Msun;
  } else {
    *M_tot = imf->stellar_particle_mass_Msun;
    *M_discrete = *M_tot;
    *M_continuous = 0;
  }
}
