/***********************************************************************//**
 * @brief Spatially integrate effective area for given energy
 *
 * @param[in] obs Observation.
 * @param[in] logE Log10 of reference energy in TeV.
 * @return Spatially integrated effective area for given energy.
 *
 * @exception GException::invalid_argument
 *            Invalid observation encountered.
 *
 * Spatially integrates the effective area for a given reference energy
 * over the region of interest.
 ***************************************************************************/
double GCTAModelAeffBackground::aeff_integral(const GObservation& obs,
                                              const double&       logE) const
{
    // Initialise result
    double value = 0.0;

    // Set number of iterations for Romberg integration.
    static const int iter_theta = 6;
    static const int iter_phi   = 6;

    // Get pointer on CTA observation
    const GCTAObservation* cta = dynamic_cast<const GCTAObservation*>(&obs);
    if (cta == NULL) {
        std::string msg = "Specified observation is not a CTA"
                          " observation.\n" + obs.print();
        throw GException::invalid_argument(G_NPRED, msg);
    }

    // Get pointer on CTA IRF response
    const GCTAResponseIrf* rsp = dynamic_cast<const GCTAResponseIrf*>(cta->response());
    if (rsp == NULL) {
        std::string msg = "Specified observation does not contain"
                          " an IRF response.\n" + obs.print();
        throw GException::invalid_argument(G_NPRED, msg);
    }

    // Retrieve pointer to CTA effective area
    const GCTAAeff* aeff = rsp->aeff();
    if (aeff == NULL) {
        std::string msg = "Specified observation contains no effective area"
                          " information.\n" + obs.print();
        throw GException::invalid_argument(G_NPRED, msg);
    }

    // Get CTA event list
    const GCTAEventList* events = dynamic_cast<const GCTAEventList*>(obs.events());
    if (events == NULL) {
        std::string msg = "No CTA event list found in observation.\n" +
                          obs.print();
        throw GException::invalid_argument(G_NPRED, msg);
    }

    // Get ROI radius in radians
    double roi_radius = events->roi().radius() * gammalib::deg2rad;

    // Setup integration function
    GCTAModelAeffBackground::npred_roi_kern_theta integrand(aeff,
                                                            logE,
                                                            iter_phi);

    // Setup integration
    GIntegral integral(&integrand);

    // Set fixed number of iterations
    integral.fixed_iter(iter_theta);

    // Spatially integrate radial component
    value = integral.romberg(0.0, roi_radius);

    // Debug: Check for NaN
    #if defined(G_NAN_CHECK)
    if (gammalib::is_notanumber(value) || gammalib::is_infinite(value)) {
        std::string origin  = "GCTAModelAeffBackground::aeff_integral";
        std::string message = " NaN/Inf encountered (value=" +
                              gammalib::str(value) + ", roi_radius=" +
                              gammalib::str(roi_radius) + ")";
        gammalib::warning(origin, message);
    }
    #endif

    // Return
    return value;

}
/***********************************************************************//**
 * @brief Return spatially integrated data model
 *
 * @param[in] obsEng Measured event energy.
 * @param[in] obsTime Measured event time.
 * @param[in] obs Observation.
 * @return Spatially integrated model.
 *
 * @exception GException::invalid_argument
 *            No CTA event list found in observation.
 *            No CTA pointing found in observation.
 *
 * Spatially integrates the data model for a given measured event energy and
 * event time. This method also applies a deadtime correction factor, so that
 * the normalization of the model is a real rate (counts/exposure time).
 ***************************************************************************/
double GCTAModelBackground::npred(const GEnergy&      obsEng,
                                  const GTime&        obsTime,
                                  const GObservation& obs) const
{
    // Initialise result
    double npred     = 0.0;
    bool   has_npred = false;

    // Build unique identifier
    std::string id = obs.instrument() + "::" + obs.id();

    // Check if Npred value is already in cache
    #if defined(G_USE_NPRED_CACHE)
    if (!m_npred_names.empty()) {

        // Search for unique identifier, and if found, recover Npred value
		// and break
		for (int i = 0; i < m_npred_names.size(); ++i) {
			if (m_npred_names[i] == id && m_npred_energies[i] == obsEng) {
				npred     = m_npred_values[i];
				has_npred = true;
				#if defined(G_DEBUG_NPRED)
				std::cout << "GCTAModelBackground::npred:";
				std::cout << " cache=" << i;
				std::cout << " npred=" << npred << std::endl;
				#endif
				break;
			}
		}

    } // endif: there were values in the Npred cache
    #endif

    // Continue only if no Npred cache value was found
    if (!has_npred) {

        // Evaluate only if model is valid
        if (valid_model()) {

            // Get CTA event list
			const GCTAEventList* events = dynamic_cast<const GCTAEventList*>(obs.events());
            if (events == NULL) {
                std::string msg = "No CTA event list found in observation.\n" +
                                  obs.print();
                throw GException::invalid_argument(G_NPRED, msg);
            }

            #if !defined(G_NPRED_AROUND_ROI)
			// Get CTA pointing direction
			GCTAPointing* pnt = dynamic_cast<GCTAPointing*>(obs.pointing());
            if (pnt == NULL) {
                std::string msg = "No CTA pointing found in observation.\n" +
                                  obs.print();
                throw GException::invalid_argument(G_NPRED, msg);
            }
            #endif

            // Get reference to ROI centre
            const GSkyDir& roi_centre = events->roi().centre().dir();

			// Get ROI radius in radians
			double roi_radius = events->roi().radius() * gammalib::deg2rad;

			// Get distance from ROI centre in radians
            #if defined(G_NPRED_AROUND_ROI)
			double roi_distance = 0.0;
            #else
			double roi_distance = roi_centre.dist(pnt->dir());
            #endif

			// Initialise rotation matrix to transform from ROI system to
            // celestial coordinate system
			GMatrix ry;
			GMatrix rz;
			ry.eulery(roi_centre.dec_deg() - 90.0);
			rz.eulerz(-roi_centre.ra_deg());
			GMatrix rot = (ry * rz).transpose();

			// Compute position angle of ROI centre with respect to model
			// centre (radians)
            #if defined(G_NPRED_AROUND_ROI)
            double omega0 = 0.0;
            #else
			double omega0 = pnt->dir().posang(events->roi().centre().dir());
            #endif

			// Setup integration function
			GCTAModelBackground::npred_roi_kern_theta integrand(spatial(),
                                                                obsEng,
                                                                obsTime,
                                                                rot,
                                                                roi_radius,
                                                                roi_distance,
                                                                omega0);

			// Setup integrator
			GIntegral integral(&integrand);
			integral.eps(1e-3);

			// Setup integration boundaries
            #if defined(G_NPRED_AROUND_ROI)
			double rmin = 0.0;
			double rmax = roi_radius;
            #else
			double rmin = (roi_distance > roi_radius) ? roi_distance-roi_radius : 0.0;
			double rmax = roi_radius + roi_distance;
            #endif

			// Spatially integrate radial component
			npred = integral.romb(rmin, rmax);

	        // Store result in Npred cache
	        #if defined(G_USE_NPRED_CACHE)
	        m_npred_names.push_back(id);
	        m_npred_energies.push_back(obsEng);
	        m_npred_times.push_back(obsTime);
	        m_npred_values.push_back(npred);
	        #endif

	        // Debug: Check for NaN
	        #if defined(G_NAN_CHECK)
	        if (gammalib::is_notanumber(npred) || gammalib::is_infinite(npred)) {
	            std::cout << "*** ERROR: GCTAModelBackground::npred:";
	            std::cout << " NaN/Inf encountered";
	            std::cout << " (npred=" << npred;
	            std::cout << ", roi_radius=" << roi_radius;
	            std::cout << ")" << std::endl;
	        }
	        #endif

        } // endif: model was valid

    } // endif: Npred computation required

	// Multiply in spectral and temporal components
	npred *= spectral()->eval(obsEng, obsTime);
	npred *= temporal()->eval(obsTime);

	// Apply deadtime correction
	npred *= obs.deadc(obsTime);

    // Return Npred
    return npred;
}
/***********************************************************************//**
 * @brief Return spatially integrated background model
 *
 * @param[in] obsEng Measured event energy.
 * @param[in] obsTime Measured event time.
 * @param[in] obs Observation.
 * @return Spatially integrated model.
 *
 * @exception GException::invalid_argument
 *            The specified observation is not a CTA observation.
 *
 * Spatially integrates the instrumental background model for a given
 * measured event energy and event time. This method also applies a deadtime
 * correction factor, so that the normalization of the model is a real rate
 * (counts/MeV/s).
 ***************************************************************************/
double GCTAModelIrfBackground::npred(const GEnergy&      obsEng,
                                     const GTime&        obsTime,
                                     const GObservation& obs) const
{
    // Initialise result
    double npred     = 0.0;
    bool   has_npred = false;

    // Build unique identifier
    std::string id = obs.instrument() + "::" + obs.id();

    // Check if Npred value is already in cache
#if defined(G_USE_NPRED_CACHE)
    if (!m_npred_names.empty()) {

        // Search for unique identifier, and if found, recover Npred value
        // and break
        for (int i = 0; i < m_npred_names.size(); ++i) {
            if (m_npred_names[i] == id && m_npred_energies[i] == obsEng) {
                npred     = m_npred_values[i];
                has_npred = true;
#if defined(G_DEBUG_NPRED)
                std::cout << "GCTAModelIrfBackground::npred:";
                std::cout << " cache=" << i;
                std::cout << " npred=" << npred << std::endl;
#endif
                break;
            }
        }

    } // endif: there were values in the Npred cache
#endif

    // Continue only if no Npred cache value has been found
    if (!has_npred) {

        // Evaluate only if model is valid
        if (valid_model()) {

            // Get pointer on CTA observation
            const GCTAObservation* cta = dynamic_cast<const GCTAObservation*>(&obs);
            if (cta == NULL) {
                std::string msg = "Specified observation is not a CTA"
                                  " observation.\n" + obs.print();
                throw GException::invalid_argument(G_NPRED, msg);
            }

            // Get pointer on CTA IRF response
            const GCTAResponseIrf* rsp = dynamic_cast<const GCTAResponseIrf*>(cta->response());
            if (rsp == NULL) {
                std::string msg = "Specified observation does not contain"
                                  " an IRF response.\n" + obs.print();
                throw GException::invalid_argument(G_NPRED, msg);
            }

            // Retrieve pointer to CTA background
            const GCTABackground* bgd = rsp->background();
            if (bgd == NULL) {
                std::string msg = "Specified observation contains no background"
                                  " information.\n" + obs.print();
                throw GException::invalid_argument(G_NPRED, msg);
            }

            // Get CTA event list
            const GCTAEventList* events = dynamic_cast<const GCTAEventList*>(obs.events());
            if (events == NULL) {
                std::string msg = "No CTA event list found in observation.\n" +
                                  obs.print();
                throw GException::invalid_argument(G_NPRED, msg);
            }

            // Get reference to ROI centre
            const GSkyDir& roi_centre = events->roi().centre().dir();

            // Get ROI radius in radians
            double roi_radius = events->roi().radius() * gammalib::deg2rad;

            // Get log10 of energy in TeV
            double logE = obsEng.log10TeV();

            // Setup integration function
            GCTAModelIrfBackground::npred_roi_kern_theta integrand(bgd, logE);

            // Setup integrator
            GIntegral integral(&integrand);
            integral.eps(g_cta_inst_background_npred_theta_eps);

            // Spatially integrate radial component
            npred = integral.romberg(0.0, roi_radius);

            // Store result in Npred cache
#if defined(G_USE_NPRED_CACHE)
            m_npred_names.push_back(id);
            m_npred_energies.push_back(obsEng);
            m_npred_times.push_back(obsTime);
            m_npred_values.push_back(npred);
#endif

            // Debug: Check for NaN
#if defined(G_NAN_CHECK)
            if (gammalib::is_notanumber(npred) || gammalib::is_infinite(npred)) {
                std::string origin  = "GCTAModelIrfBackground::npred";
                std::string message = " NaN/Inf encountered (npred=" +
                                      gammalib::str(npred) + ", roi_radius=" +
                                      gammalib::str(roi_radius) + ")";
                gammalib::warning(origin, message);
            }
#endif

        } // endif: model was valid

    } // endif: Npred computation required

    // Multiply in spectral and temporal components
    npred *= spectral()->eval(obsEng, obsTime);
    npred *= temporal()->eval(obsTime);

    // Apply deadtime correction
    npred *= obs.deadc(obsTime);

    // Return Npred
    return npred;
}