/* -*- mia-c++  -*-
 *
 * This file is part of MIA - a toolbox for medical image analysis
 * Copyright (c) Leipzig, Madrid 1999-2017 Gert Wollny
 *
 * MIA 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 MIA; if not, see <http://www.gnu.org/licenses/>.
 *
 */

#include <cmath>
#include <climits>
#include <iomanip>


#include <mia/internal/autotest.hh>
#include <mia/core/noisegen.hh>
#include <mia/core/mitestimages.hh>
#include <mia/core/splineparzenmi.hh>
#include <boost/filesystem.hpp>

NS_MIA_USE;
using namespace std;
using namespace boost::unit_test;
namespace bfs = boost::filesystem;

struct SplineMutualInformationFixture  {
       SplineMutualInformationFixture();

       size_t size;
       vector<double> reference;
       vector<double> moving;
       size_t bins;
       PSplineKernel rkernel;
       PSplineKernel mkernel;


};


BOOST_FIXTURE_TEST_CASE( test_same_image_entropy, SplineMutualInformationFixture )
{
       PSplineKernel haar = CSplineKernelPluginHandler::instance().produce("bspline:d=0");
       CSplineParzenMI smi(256, haar, 256, haar, 0);
       smi.fill(reference.begin(), reference.end(), reference.begin(), reference.end());
       BOOST_CHECK_CLOSE(smi.value(), -5.1013951881429653, 0.1);
}


BOOST_FIXTURE_TEST_CASE( test_different_image_entropy, SplineMutualInformationFixture )
{
       PSplineKernel haar = CSplineKernelPluginHandler::instance().produce("bspline:d=0");
       CSplineParzenMI smi(256, haar, 256, haar, 0);
       smi.fill(moving.begin(), moving.end(), reference.begin(), reference.end());
       // this test value is not really evaluated ...
       BOOST_CHECK_CLOSE(smi.value(), -1.1739765269855784, 0.1);
}


/** When filling the histogram by using splie coefficients, the numeric gradient will not be zero
    unless the image intensities are uniformly distributed.
BOOST_FIXTURE_TEST_CASE( test_same_image_gradient_is_zero, SplineMutualInformationFixture )
{
        CSplineParzenMI smi(253, rkernel, 253, mkernel, 0);
	smi.fill(moving.begin(), moving.end(), reference.begin(), reference.end());
	for (double m = 2; m < 254; m += 1)
		for (double r = 0; r < 256; r += 1)
			BOOST_CHECK_EQUAL(smi.get_gradient_slow(m,r), 0.0);

}
*/


BOOST_FIXTURE_TEST_CASE( test_differnt_image_gradient_is_not_zero, SplineMutualInformationFixture )
{
       CSplineParzenMI smi(bins, rkernel, bins, mkernel, 0);
       smi.fill(moving.begin(), moving.end(), reference.begin(), reference.end());
       int cnt = 0;

       for (double m = 2; m < 254; m += 1)
              for (double r = 0; r < 256; r += 1) {
                     if (smi.get_gradient(m, r) != 0.0)
                            ++cnt;
              }

       BOOST_CHECK(cnt > 0);
       cvdebug() << "nozero gradient values =" << cnt << "\n";
}


BOOST_FIXTURE_TEST_CASE( test_different_image_gradient, SplineMutualInformationFixture )
{
       CSplineParzenMI smi(bins, rkernel, bins, mkernel, 0);
       smi.fill(moving.begin(), moving.end(), reference.begin(), reference.end());
       int cnt = 0;

       for (double m = 0; m < 256; m += 1)
              for (double r = 0; r < 256; r += 1) {
                     if (smi.get_gradient(m, r) != 0.0)
                            ++cnt;

                     cvdebug() << "( "
                               << setw(3) << m << ","
                               << setw(3) << r << ") = "
                               << smi.get_gradient(m, r) << "\n";
              }

       BOOST_CHECK(cnt > 0);
       cvdebug() << "nozero gradient values =" << cnt << "\n";
}
SplineMutualInformationFixture::SplineMutualInformationFixture():
       size(mi_test_size.width * mi_test_size.height),
       reference(reference_init_data, reference_init_data + size),
       moving(moving_init_data, moving_init_data + size),
       bins(128)
{
       rkernel = CSplineKernelPluginHandler::instance().produce("bspline:d=0");
       mkernel = CSplineKernelPluginHandler::instance().produce("bspline:d=3");
}


class CSplineParzenMIFixture: public CSplineParzenMI
{
public:
       CSplineParzenMIFixture();
protected:
       double p(double r, double m) const;
       double px(double x) const;
       double py(double x) const;
       double dm(double r, double m) const;
       double mi_direct()const;
       double entropy_direct(const vector <double>& p)const;
       static const int bins = 128;
       // values obtained by using (wx)maxima
       static constexpr double HX = -4.847144258909647;
       static constexpr double HY = -4.699286674800261;
       static constexpr double HXY = -9.528771470298688;
       static constexpr double MI =  HX + HY - HXY;

       vector<double> m_hm;
       vector<double> m_hr;
       vector<double> m_values;
};

constexpr double CSplineParzenMIFixture::HX;
constexpr double CSplineParzenMIFixture::HY;
constexpr double CSplineParzenMIFixture::HXY;
constexpr double CSplineParzenMIFixture::MI;


CSplineParzenMIFixture::CSplineParzenMIFixture():
       CSplineParzenMI(bins, produce_spline_kernel("bspline:d=0"),
                       bins, produce_spline_kernel("bspline:d=3"), 0),
       m_hm(bins),
       m_hr(bins),
       m_values(bins * bins)
{
       auto iv = m_values.begin();

       for (int r = 0; r < bins; ++r) {
              for (int m = 0; m < bins; ++m, ++iv) {
                     *iv = p(r, m);
                     m_hm[m] += *iv;
                     m_hr[r] += *iv;
              }
       }

       fill_histograms(m_values, 0, bins - 1, 0, bins - 1);
}

double CSplineParzenMIFixture::mi_direct() const
{
       return entropy_direct(m_hm) + entropy_direct(m_hr) - entropy_direct(m_values);
}

double CSplineParzenMIFixture::entropy_direct(const vector <double>& p)const
{
       double sum = 0.0;

       for (auto i = p.begin(); i != p.end(); ++i)
              if (*i > 0.0)
                     sum += *i * std::log(*i);

       return sum;
}

double CSplineParzenMIFixture::p(double y, double x) const
{
       constexpr double scale = M_PI * M_PI * M_PI * M_PI / (M_PI - 4) / 4294967296.0;
       return  scale * cos(M_PI / 256.0 * (x - y)) * (y - 128) * y;
}

double CSplineParzenMIFixture::px(double x) const
{
       const double pxh = M_PI * x / 256.0;
       return M_PI / 512.0 * (sin(pxh) + cos(pxh));
}




double CSplineParzenMIFixture::py(double y) const
{
       constexpr double scale = M_PI * M_PI * M_PI / (16777216 * (M_PI - 4.0));
       const double pyh = M_PI * y / 256.0;
       const double pymh = M_PI * (y - 128)  / 256.0;
       return scale * (y - 128 ) * y * ( sin(pyh) - sin(pymh) );
}

double CSplineParzenMIFixture::dm(double y, double x) const
{
       // clang 3.2 doesn't like pow(x,y) in a costexpr.
       constexpr double pi4 = M_PI * M_PI * M_PI * M_PI;
       constexpr double scale_p = M_PI * M_PI * M_PI * M_PI * M_PI / (1099511627776.0 * (M_PI - 4));
       constexpr double scale_px = M_PI * M_PI / 131072.0;
       const double pyxh = M_PI * (y - x)  / 256.0;
       const double pxh = M_PI * x  / 256.0;
       const double dplogp_dx =  scale_p * y * (y - 128) * sin(pyxh) *
                                 (std::log( pi4 * (y - 128) * y * cos(pyxh) /
                                            (4294967296 * (M_PI - 4))) + 1);
       const double dpxlogopx_dx = - scale_px * (std::log(px(x)) + 1) * ( sin(pxh) - cos(pxh));
       return -dplogp_dx + dpxlogopx_dx;
}

inline double f_log(double x)
{
       return x * std::log(x);
}

BOOST_FIXTURE_TEST_CASE( test_self, CSplineParzenMIFixture )
{
       for (double m = 1; m < (bins - 1); m += 1)
              for (double r = 1; r < (bins - 1); r += 1) {
                     double self_grad = dm(r, m );
                     double dmp = 5 * (f_log(p(r, m + 0.1)) - f_log(p(r, m - 0.1)));
                     double dmpx = 5 * (f_log(px(m + 0.1)) - f_log(px(m - 0.1)));
                     double test_grad = dmpx - dmp;
                     cvdebug() << self_grad << " vs " << test_grad << ":" << self_grad / test_grad << "\n";

                     // if the gradient is very small test against 'close to zero'
                     if (fabs(test_grad) > 1e-16)
                            BOOST_CHECK_CLOSE(self_grad, test_grad, 0.1);
                     else
                            BOOST_CHECK(fabs(self_grad) < 1e-16);
              }
}



BOOST_FIXTURE_TEST_CASE( test_MI_random, CSplineParzenMIFixture )
{
       BOOST_CHECK_CLOSE(value(), mi_direct(), 0.3);
       BOOST_CHECK_CLOSE(value(), MI, 0.3);

       // at the boundary of the "moving" gradient we are more forgiving, since here
       // the analytic function is not well modeled by the parzen spline histogram
       // hence, do not test the first and last two indices.
       // otoh for the reference values, the gradient at the boundary is NAN
       for (double m = 2; m < (bins - 2); m += 1)
              for (double r = 1; r < bins - 1; r += 1) {
                     double grad = get_gradient_slow(m, r);
                     double test_grad = dm(r, m );

                     // at 64,64 the gradient is very close to zero
                     if ((fabs(grad) > 1e-10) || (fabs(test_grad) > 1e-10))
                            BOOST_CHECK_CLOSE(grad, test_grad, 0.1);

                     cvdebug() << "Q=" << test_grad / grad << "\n";
              }
}

BOOST_FIXTURE_TEST_CASE( test_MI_76_13, CSplineParzenMIFixture )
{
       double grad = get_gradient_slow(76, 13);
       double test_grad = dm(13, 76 );
       BOOST_CHECK_CLOSE(grad, test_grad, 0.1);
}
