/*
 * Copyright (C) 2008-2009 Sébastien Villemot <sebastien.villemot@ens.fr>
 *
 * This program 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
 * (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 General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <vector>
#include <ostream>

#include <gsl/gsl_vector.h>
#include <gsl/gsl_matrix.h>
#include <gsl/gsl_qrng.h>

#include "ModelSpec.hh"
#include "ModelSolution.hh"

class SolutionTester
{
public:
  enum integration_method
    {
      GaussHermiteAndMonomialDegree5,
      GaussHermiteOnly,
      MonomialDegree5Only,
      QuasiMonteCarloOnly
    };

private:
  ModelSolution &modsol;
  //! Number of countries
  const int n;
  //! Number of variables
  const int ny;
  //! Numerical integration method to be used
  const integration_method int_meth;
  //! Whether to dump CSV files with simulations
  const bool dump;

  //! Thrown when matrix is not inversible in prewithen() or in inv_newey_west_estimator()
  class SingularityException
  {
  };

  //! Number of nodes used for Gauss-Hermite integration
  const static int HERMITE_NODES_NB = 4;
  //! Nodes for Gauss-Hermite integration (roots of Hermite polynomials)
  static const double hermite_nodes[HERMITE_NODES_NB];
  //! Weights for Gauss-Hermite integration
  static const double hermite_weights[HERMITE_NODES_NB];
  //! Computes Euler errors using product Gauss-Hermite integration
  /*! \todo Should value functions be sorted before adding them ? */
  void euler_errors_gauss_hermite(const gsl_vector *y_prev, const gsl_vector *y_curr, const gsl_vector *shocks_curr, gsl_vector *errors);

  //! Constants used in monomial degree 5 integration formula
  const double monomial_deg5_d, monomial_deg5_r, monomial_deg5_s, monomial_deg5_V, monomial_deg5_A, monomial_deg5_B, monomial_deg5_D;
  //! Computes Euler errors using monomial degree 5 integration formula
  void euler_errors_monomial_deg5(const gsl_vector *y_prev, const gsl_vector *y_curr, const gsl_vector *shocks_curr, gsl_vector *errors);

  //! Number of points for Quasi Monte-Carlo integration
  const static int QMC_POINTS_NB = 1000;
  //! Computes Euler errors using Quasi Monte-Carlo integration (low discrepancy sequence)
  /*! \todo Should value functions be sorted before adding them ? */
  void euler_errors_quasi_monte_carlo(const gsl_vector *y_prev, const gsl_vector *y_curr, const gsl_vector *shocks_curr, gsl_vector *errors);

  //! Dimensionality above which we switch to Quasi Monte-Carlo
  const static int MAX_GAUSS_HERMITE_DIM = 6;
  //! Computes Euler errors, selecting between possible numerical methods
  void euler_errors(const gsl_vector *y_prev, const gsl_vector *y_curr, const gsl_vector *shocks_curr, gsl_vector *errors);

  //! Computes simulations of the model
  /*!
    \param init an initial vector of ny variables; if warmup>0, it will be updated to contain the variables vector just before the "real" simulations begin
    \param sims the computed simulations (T*ny matrix)
    \param shocks the shocks drawn for the simulation (T*(n+1) matrix)
    \param warmup the number of periods to simulate before actually filling sims and shocks
    \param rng_seed seed for the random number generator
  */
  void simulate_model(gsl_vector *init, gsl_matrix *sims, gsl_matrix *shocks, int warmup, unsigned long rng_seed);
  //! Does prewhitening of a time-series (fits a VAR(1))
  /*!
    \param h the observations in a T*r matrix
    \param A the r*r matrix of VAR(1) coefficients
    \param resid the computed residuals in a T*r matrix; note that the first line is not usable, since there is no computable residual for first date
  */
  void prewhiten(const gsl_matrix *h, gsl_matrix *A, gsl_matrix *resid) throw (SingularityException);
  //! Computes bandwith for estimator of spectral density at zero
  /*! Follows the computations and the notations of Newey and West (1994)
    \param w the weight vector (size r)
    \param resid the residuals of the prewithening (T*r matrix)
    \return The bandwith parameter
    Note that this method ignores the first line of resid, since they are supposed to be unitialized.
  */
  int newey_west_bandwith(const gsl_vector *w, const gsl_matrix *resid);

  //! Computes the matrix inverse of Newey and West (1994) estimator of the spectral density at zero
  void inv_newey_west_estimator(const gsl_matrix *resid, gsl_matrix *inv_estim) throw (SingularityException);
#ifdef DEBUG
  static void print_matrix(const gsl_matrix *A);
#endif

  //! Quasi-randomly draw a multivariate standard normal vector
  static void gaussian_draw(gsl_vector *x, gsl_qrng *qrng);
  //! Quasi-randomly draw a point on a hypersphere
  /*!
    \param x the random draw
    \param center the hypersphere center
    \param radius the hypersphere radius
    \param qrng quasi-random number generator
  */
  static void hypersphere_draw(gsl_vector *x, const gsl_vector *center, double radius, gsl_qrng *qrng);

  // Generate instruments for DenHaan-Marcet statistics
  /*!
    \param init the variables vector used as starting point for simulations
    \param sims the generated simulations
    \param constant include a constant as instrument
    \param order1_monomials include first order monomials of state variables (a_t and k_{t-1}) as instruments
    \param order2_monomials include second order monomials of state variables
    \return A matrix with as many rows as sims, containing the instruments
  */
  gsl_matrix *generate_instruments(const gsl_vector *init, const gsl_matrix *sims, bool constant,
                                   bool order1_monomials, bool order2_monomials);
public:
  typedef std::vector<std::vector<int> > equation_blocks_type;

private:
  // DenHaan-Marcet statistics, one p-value per equation block
  /*!
    \param T number of periods for each simulation
    \param warmup number of periods to discard at the beginning of each simulation
    \param equation_blocks list of blocks of equation numbers on which to compute the statistics
    \param constant use a constant as instrument
    \param order1_monomials use first order monomials as instruments
    \param order2_monomials use second order monomials as instruments
    \param pvals the resulting p-values for DHM stats
    \param rnd_seed a seed for the random generator; for generating simulations, seeds in the range [rng_seed, rng_seed+n-1] will be used.
  */
  void den_haan_marcet_stat(int T, int warmup, const equation_blocks_type &equation_blocks,
                            bool constant, bool order1_monomials, bool order2_monomials,
                            gsl_vector *pvals, unsigned long rng_seed);

  void write_variables_head(std::ostream &output) const;
  void write_residuals_head(std::ostream &output) const;

public:
  SolutionTester(ModelSolution &modsol_arg, integration_method int_meth_arg, bool dump_arg);
  //! Accuracy test 1: Euler equation errors on points of a hypersphere around steady state
  /*!
    \param nb_points number of points drawn on the sphere
    \param radius radius of the sphere
    \param max_err on output, maximum error over all points and all equations (in base 10 log)
    \param eq_max_err if not NULL, on output, maximum error over all points for each equation (in base 10 log)
  */
  void accuracy_test1(int nb_points, double radius, double &max_err, gsl_vector *eq_max_err = NULL);
  //! Accuracy test 2: Dynamic Euler equations errors
  /*!
    \param T number of periods for each simulation
    \param warmup number of periods to discard at the beginning of each simulation
    \param max_err on output, maximum error over all periods and all equations (in base 10 log)
    \param mean_err on output, mean error over all periods and all equations (in base 10 log)
    \param eq_max_err if not NULL, on output, maximum error over all periods for each equation (in base 10 log)
    \param rng_seed a seed for the random generator
  */
  void accuracy_test2(int T, int warmup, double &max_err, double &mean_err, gsl_vector *eq_max_err, unsigned long rng_seed);
  //! Accuracy test 3: Den Haan - Marcet statistics
  /*!
    \param nstat number of simulations to perform
    \param T number of periods for each simulation
    \param warmup number of periods to discard at the beginning of each simulation
    \param equation_blocks list of blocks of equation numbers on which to compute the statistics
    \param constant use a constant as instrument
    \param order1_monomials use first order monomials of state variables as instruments
    \param order2_monomials use second order monomials of state variables as instruments
    \param thresholds p-values of the chi-square distribution used as benchmark for the DHM stat
    \param freqs on output, empirical CDF of the DHM stat at the p-values given in "thresholds": each column corresponds to a block of equations, each row to a given p-value
    \param rnd_seed a seed for the random generator; for generating simulations, seeds in the range [rng_seed, rng_seed+n-1] will be used.
  */
  void accuracy_test3(int nstat, int T, int warmup, const equation_blocks_type &equation_blocks,
                      bool constant, bool order1_monomials, bool order2_monomials,
                      const gsl_vector *thresholds, gsl_matrix *freqs, unsigned long rng_seed);
#ifdef DEBUG
  static void hypersphere_draw_test(int dim, double zmin, double zmax, int ndraw);
#endif
};
