/* -*- 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 <iostream>
#include <iomanip>
#include <string>
#include <limits>
#include <sstream>
#include <stdexcept>

#include <mia/internal/main.hh>
#include <mia/2d/filter.hh>
#include <mia/2d/imageio.hh>
#include <mia/2d/segsetwithimages.hh>
#include <mia/core.hh>

using namespace std;
using namespace mia;

const SProgramDescription g_description = {
       {pdi_group, "Tools for Myocardial Perfusion Analysis"},

       {pdi_short, "Evaluate pixel-wise time-intensity gradient of a 2D image series."},

       {
              pdi_description, "Given a set of images of temporal sucession, evaluate the temporal "
              "pixel-wise gaussian and evaluate pixel-wise its MAD."
              "A spacial pre-filtering may be applied by specifying additional plugins "
              "(filter/2dimage)"
       },

       {
              pdi_example_descr, "Evaluate the MAD-image of the bounding box surrounding the segmentation "
              "from a series segment.set after applying a temporal Gaussian "
              "filter of width 5. No spacial filtering will be applied. "
              "The bounding box will be enlarged by 3 pixels in all directions. "
              "Store the image in OpenEXR format."
       },

       {pdi_example_code, "-i segment.set -o mad.exr -g 2 -c -e 3"}
};

template <typename T>
struct fabsdelta {
       T operator () (T x, T y) const
       {
              return fabs(x - y);
       }
};
struct C2DVarAccumulator : public TFilter<bool> {
       typedef vector<double> CBuffer;

       C2DVarAccumulator(size_t n, size_t gauss_width):
              m_min(numeric_limits<float>::max()),
              m_max(-numeric_limits<float>::max()),
              m_n(n - 1),
              m_initialized(false)
       {
              stringstream gauss_descr;
              gauss_descr << "gauss:w=" << gauss_width;
              gauss = C1DSpacialKernelPluginHandler::instance().produce(gauss_descr.str().c_str());
       }

       template <typename T>
       bool operator ()(const T2DImage<T>& image)
       {
              if (!m_initialized) {
                     m_field.resize(image.size());
                     m_size = image.get_size();
                     m_initialized = true;
              }

              if (image.get_size() != m_size)
                     throw invalid_argument("Input images are not all of same size");

              auto i = image.begin();
              auto e = image.end();
              auto v = m_field.begin();

              for (; i != e; ++i, ++v)
                     v->push_back(*i);

              auto src_minmax = minmax_element(image.begin(), image.end());

              if (m_min > *src_minmax.first)
                     m_min = *src_minmax.first;

              if (m_max < *src_minmax.second)
                     m_max = *src_minmax.second;

              return true;
       }

       void evaluate_gradients()
       {
              auto v = m_field.begin();
              auto e = m_field.end();

              while ( v != e ) {
                     gauss->apply_inplace(*v);
                     CBuffer gradient(v->size() - 1);
                     transform(v->begin() + 1, v->end(), v->begin(), gradient.begin(),
                     [](double x, double y) {
                            return fabs(x - y);
                     });
                     *v = gradient;
                     ++v;
              }
       }

       static float median( CBuffer::iterator begin, CBuffer::iterator end, size_t len)
       {
              if (len & 1) {
                     auto i = begin + (len - 1) / 2;
                     nth_element(begin, i, end);
                     return *i;
              } else {
                     auto i1 = begin + len / 2 - 1;
                     auto i2 = begin + len / 2;
                     nth_element(begin, i1, end);
                     nth_element(begin, i2, end);
                     return (*i1 + *i2) / 2.0;
              }
       }

       P2DImage  result()
       {
              evaluate_gradients();
              float range = 255.0 / (m_max - m_min);
              C2DFImage *variation = new C2DFImage(m_size);
              auto ii = variation->begin();
              auto iv = m_field.begin();
              auto ev = m_field.end();

              while (iv != ev) {
                     *ii++ = median(iv->begin(), iv->end(), m_n);
                     ++iv;
              }

              ii = variation->begin();
              iv = m_field.begin();

              while (iv != ev) {
                     auto ip = iv->begin();
                     auto ep = iv->end();

                     while (ip != ep) {
                            *ip = fabs(*ip - *ii);
                            ++ip;
                     }

                     ++iv;
                     ++ii;
              }

              ii = variation->begin();
              iv = m_field.begin();

              while (iv != ev) {
                     *ii++ = median(iv->begin(), iv->end(), m_n) * range;
                     ++iv;
              }

              return P2DImage(variation);
       }

private:
       C2DBounds m_size;
       float m_min;
       float m_max;
       vector<CBuffer> m_field;
       size_t m_n;
       bool m_initialized;
       std::shared_ptr<C1DFilterKernel > gauss;
};

int do_main( int argc, char *argv[] )
{
       string in_filename;
       string out_filename;
       string out_type;
       bool crop;
       size_t skip = 0;
       size_t enlarge_boundary = 5;
       size_t gauss_width = 1;
       const auto& imageio = C2DImageIOPluginHandler::instance();
       CCmdOptionList options(g_description);
       options.add(make_opt( in_filename, "in-file", 'i', "input segmentation set", CCmdOptionFlags::required_input));
       options.add(make_opt( out_filename, "out-file", 'o', "output file name", CCmdOptionFlags::required_output, &imageio));
       options.add(make_opt( skip, "skip", 'k', "Skip files at the beginning"));
       options.add(make_opt( enlarge_boundary,  "enlarge-boundary", 'e', "Enlarge cropbox by number of pixels"));
       options.add(make_opt( crop, "crop", 'c', "crop image before running statistics"));
       options.add(make_opt( gauss_width, "gauss", 'g', "gauss filter width for moothing the gradient"));

       if (options.parse(argc, argv, "filter", &C2DFilterPluginHandler::instance()) != CCmdOptionList::hr_no)
              return EXIT_SUCCESS;

       C2DImageFilterChain filter_chain(options.get_remaining());
       cvdebug() << "IO supported types: " << imageio.get_plugin_names() << "\n";
       CSegSetWithImages  segset(in_filename, true);

       if (crop) {
              C2DBoundingBox box = segset.get_boundingbox();
              box.enlarge(enlarge_boundary);
              stringstream crop_descr;
              crop_descr << "crop:"
                         << "start=[" << box.get_grid_begin()
                         << "],end=[" << box.get_grid_end() << "]";
              cvdebug() << "Crop with " << crop_descr.str() << "\r";
              filter_chain.push_front(crop_descr.str().c_str());
       }

       if (skip >= segset.get_images().size())
              throw invalid_argument("Skip is equal or larger then image series");

       auto iimages = segset.get_images().begin();
       auto  eimages = segset.get_images().end();
       advance(iimages, skip);
       C2DVarAccumulator acc(distance(iimages, eimages), gauss_width);

       for (; iimages != eimages; ++iimages) {
              P2DImage in_image = *iimages;

              if (!filter_chain.empty() )
                     in_image = filter_chain.run(in_image);

              mia::accumulate(acc, *in_image);
       }

       if (save_image(out_filename, acc.result()))
              return  EXIT_SUCCESS;

       return EXIT_FAILURE;
}

MIA_MAIN(do_main)
