/* -*- 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 <map>
#include <algorithm>
#include <stdexcept>
#include <cmath>

#include <mia/core/msgstream.hh>

#include <boost/algorithm/minmax_element.hpp>


#include <mia/core/slopeclassifier.hh>
#include <mia/core/slopestatistics.hh>


NS_MIA_BEGIN

using namespace std;
using namespace boost;


struct CSlopeClassifierImpl {
       int RV_peak;
       int LV_peak;
       int RV_idx;
       int LV_idx;
       int Periodic_idx;
       int Perfusion_idx;
       int Baseline_idx;
       float max_slope_length_diff;
       CSlopeClassifier::SCorrelation  selfcorr;

       typedef vector<float>::const_iterator position;
       typedef pair<position, position> extrems;
       typedef pair<size_t, size_t> extrems_pos;
       CSlopeClassifierImpl(const CSlopeClassifier::Columns& series, bool mean_stripped);
       CSlopeClassifierImpl();
private:
       void evaluate_selfcorr(const CSlopeClassifier::Columns& series);
};


CSlopeClassifier::CSlopeClassifier(const CSlopeClassifier::Columns& m, bool mean_stripped)
{
       if (m.size() < 3)
              throw invalid_argument("CSlopeClassifier: require at least 3 curves");

       impl = new CSlopeClassifierImpl(m, mean_stripped);
}

CSlopeClassifier::CSlopeClassifier(const CSlopeClassifier& other):
       impl(new CSlopeClassifierImpl(*other.impl))
{
}
CSlopeClassifier::CSlopeClassifier():
       impl(new CSlopeClassifierImpl())
{
}

CSlopeClassifier& CSlopeClassifier::operator =(const CSlopeClassifier& other)
{
       if (this != &other) {
              auto help = new CSlopeClassifierImpl(*other.impl);
              delete impl;
              impl = help;
       }

       return *this;
}

CSlopeClassifier::~CSlopeClassifier()
{
       delete impl;
}

float CSlopeClassifier::get_max_slope_length_diff() const
{
       assert(impl);
       return impl->max_slope_length_diff;
}

CSlopeClassifier::SCorrelation CSlopeClassifier::max_selfcorrelation()const
{
       return impl->selfcorr;
}

int CSlopeClassifier::get_periodic_idx() const
{
       return impl->Periodic_idx;
}

int CSlopeClassifier::get_perfusion_idx() const
{
       return impl->Perfusion_idx;
}


int CSlopeClassifier::get_RV_idx()const
{
       return impl->RV_idx;
}

int CSlopeClassifier::get_LV_idx() const
{
       return impl->LV_idx;
}

int CSlopeClassifier::get_RV_peak()const
{
       return impl->RV_peak;
}

int CSlopeClassifier::get_LV_peak() const
{
       return impl->LV_peak;
}

int CSlopeClassifier::get_baseline_idx() const
{
       return impl->Baseline_idx;
}


typedef pair<PSlopeStatistics, int> statmap;

struct compare_length {
       bool operator () (const statmap& a, const statmap& b) const
       {
              return  (a.first->get_curve_length() < b.first->get_curve_length());
       }
};

struct compare_range {
       bool operator () (const statmap& a, const statmap& b) const
       {
              return  (a.first->get_range() > b.first->get_range());
       }
};

struct compare_perfusion_peak {
       bool operator () (const statmap& a, const statmap& b) const
       {
              return  (a.first->get_perfusion_high_peak() < b.first->get_perfusion_high_peak());
       }
};

struct compare_mean_freq {
       bool operator () (const statmap& a, const statmap& b) const
       {
              return  (a.first->get_mean_frequency() < b.first->get_mean_frequency());
       }
};



float  EXPORT_CORE correlation(const vector<float>& a, const vector<float>& b)
{
       assert(a.size() > 0);
       assert(a.size() == b.size());
       float sxx = 0.0;
       float syy = 0.0;
       float sxy = 0.0;
       float sx =  0.0;
       float sy =  0.0;

       for (size_t i = 0; i < a.size(); ++i) {
              sx += a[i];
              sy += b[i];
              sxx += a[i] * a[i];
              syy += b[i] * b[i];
              sxy += a[i] * b[i];
       }

       const float ssxy = sxy - sx * sy / a.size();
       const float ssxx = sxx - sx * sx / a.size();
       const float ssyy = syy - sy * sy / a.size();

       if (sxx == 0 && syy == 0)
              return 1.0;

       if (sxx == 0 || syy == 0)
              return 0.0;

       return (ssxy * ssxy) /  (ssxx * ssyy);
}

void CSlopeClassifierImpl::evaluate_selfcorr(const CSlopeClassifier::Columns& series)
{
       selfcorr.corr = 0.0;

       for (size_t i = 0; i < series.size(); ++i)
              for (size_t j = i + 1; j < series.size(); ++j) {
                     const float corr = correlation(series[i], series[j]);

                     if (selfcorr.corr < corr) {
                            selfcorr.row1 = i;
                            selfcorr.row2 = j;
                            selfcorr.corr = corr;
                     }
              }
}


CSlopeClassifierImpl::CSlopeClassifierImpl():
       RV_peak(-1),
       LV_peak(-1),
       RV_idx(-1),
       LV_idx(-1),
       Periodic_idx(-1),
       Perfusion_idx(-1),
       Baseline_idx(-1),
       max_slope_length_diff(0)
{
       selfcorr.row1 = 0;
       selfcorr.row2 = 0;
       selfcorr.corr = 0.0;
}

CSlopeClassifierImpl::CSlopeClassifierImpl(const CSlopeClassifier::Columns& series, bool mean_stripped):
       Periodic_idx(-1),
       Perfusion_idx(-1),
       Baseline_idx(-1)
{
       size_t n = series.size();
       vector<statmap> stats(n);

       for (size_t i = 0; i < n; ++i) {
              statmap sm;
              sm.first = PSlopeStatistics(new CSlopeStatistics(series[i], i));
              sm.second = i;
              stats[i] = sm;
       }

       /* mechanics for classifying the mixing curves:
          - first sort for curve length - longest is periodic component
          - use a heuristic based on its mean frequency to decide whether this curve is actually periodic
          - then sort the remaining curves by covered value range
             * the curves that cover largest values should correspond to LV and RV enhancement
                (Remark: this is not save!!!)
       	* remove last element from the serach range, it's baseline or perfusion
          - sort for minimal mean frequency, RV-LV slopes should have a low freq
           -
           - sort these two curves by the order in which the high peaks appear to identify
             which is RV (peak comes first) and which LV
       */
       int sort_skip = 0;
       sort(stats.begin(), stats.end(), compare_length());
       max_slope_length_diff = stats[n - 1].first->get_curve_length() - stats[n - 2].first->get_curve_length();
       float rate = series[0].size() / 7.0f;
       bool has_periodic = stats[n - 1].first->get_mean_frequency() > rate ;
       cvinfo() << stats[n - 1].first->get_mean_frequency() << " vs " << rate << "=" << has_periodic << "\n";

       if (has_periodic && (mean_stripped || n > 3)) {
              // put the periodic element at the end
              cvinfo() << "identify periodic as " << stats[n - 1].second << "\n";
              ++sort_skip;
       } else
              has_periodic = false;

       if (n - sort_skip > 2) {
              sort(stats.begin(), stats.end() - sort_skip, compare_mean_freq());

              for (auto k = stats.rbegin() + sort_skip; k != stats.rend(); ++k)
                     if (k->first->get_mean_frequency() > rate) {
                            cvinfo() << "identify " << k->second << "as too periodic\n";
                            ++sort_skip;
                     }
       }

       int remaining = n - sort_skip;
       cvdebug() << "remaining = " << remaining << "\n";

       if (remaining > 2 ) {
              cvdebug() << "range sort\n";
              sort(stats.begin(), stats.end() - sort_skip, compare_range());
              int of_interest = 2;
              double max_range = stats[0].first->get_range();

              while (of_interest < remaining &&
                     max_range - stats[of_interest].first->get_range() < 0.5 * max_range)
                     ++of_interest;

              remaining = of_interest;
       }

       cvdebug() << "remaining = " << remaining << "\n";
       sort(stats.begin(), stats.begin() + remaining, compare_perfusion_peak());
       cvinfo() << "Sorted\n";

       for (size_t i = 0; i < n; ++i) {
              cvinfo() << "Stats[" << stats[i].second << "]"
                       << " range: " << stats[i].first->get_range()
                       << " perfpeakidx: " << stats[i].first->get_perfusion_high_peak().first
                       << " length: " << stats[i].first->get_curve_length()
                       << " l*r: " << stats[i].first->get_curve_length() * stats[i].first->get_range()
                       << " l/r: " << stats[i].first->get_curve_length() / stats[i].first->get_range()
                       << " f: " << stats[i].first->get_mean_frequency()
                       << "\n";
       }

       RV_idx = stats[0].second;
       LV_idx = stats[1].second;

       if (mean_stripped) {
              Periodic_idx = stats[n - 1].second;

              if (n > 3)
                     Perfusion_idx = stats[2].second;

              if (n > 4)
                     Baseline_idx = stats[3].second;
       } else {
              if (n == 3)
                     Baseline_idx = stats[n - 1].second;
              else {
                     Periodic_idx = stats[n - 1].second;
                     Baseline_idx = stats[n - 2].second;

                     if (n > 4)
                            Perfusion_idx = stats[2].second;
              }
       }

       // was not really a periodic component
       if (!has_periodic)
              Periodic_idx = -1;

       RV_peak = stats[0].first->get_perfusion_high_peak().first;
       LV_peak = stats[1].first->get_perfusion_high_peak().first;
       evaluate_selfcorr(series);
}

NS_MIA_END
