/* -*- 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/>.
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <mia/2d/imageio.hh>
#include <mia/2d/angle.hh>
#include <mia/2d/segframe.hh>
#include <mia/core/msgstream.hh>
#include <mia/core/errormacro.hh>
#include <mia/core/xmlinterface.hh>

#include <boost/filesystem.hpp>

#include <stdexcept>
#include <string>


NS_MIA_BEGIN
using namespace std;

namespace bfs = boost::filesystem;

CSegFrame::CSegFrame():
       m_has_star(false),
       m_quality(0),
       m_brightness(0),
       m_contrast(0),
       m_version(0)
{
}

CSegFrame::CSegFrame(const string& image, const CSegStar& star, const Sections& sections):
       m_has_star(true),
       m_star(star),
       m_sections(sections),
       m_filename(image),
       m_quality(0),
       m_brightness(0),
       m_contrast(0),
       m_version(0)

{
}


template <typename T>
void read_attribute_from_node(const CXMLElement& elm, const std::string& key, T& out_value, bool required)
{
       auto attr = elm.get_attribute(key);

       if (attr.empty()) {
              if (required)
                     throw create_exception<std::runtime_error>( elm.get_name(), ":required attribute '", key, "' not found");
              else
                     return;
       }

       if (!from_string(attr, out_value))
              throw create_exception<std::runtime_error>( elm.get_name(), ":attribute '", key, "' has bogus value '",
                            attr, "'");
}


CSegFrame::CSegFrame(const CXMLElement& elm, int version):
       m_has_star(false),
       m_quality(0),
       m_brightness(0),
       m_contrast(0),
       m_version(version)
{
       TRACE("CSegFrame::CSegFrame");

       if (elm.get_name() != "frame")
              throw invalid_argument(string("CSegFrame: unexpected node type: ") + elm.get_name());

       m_filename = elm.get_attribute("image");

       if (m_filename.empty()) {
              throw invalid_argument("CSegFrame: image attribute not found");
       }

       auto nodes = elm.get_all_children();

       for (auto i : nodes) {
              if (i->get_name() == "star") {
                     m_star = CSegStar(*i);
                     m_has_star = true;
              } else if (i->get_name() == "section") {
                     m_sections.push_back(CSegSection(*i, version));
              } else {
                     cvinfo() << "ignoring unsupported element '" << i->get_name() << "'\n";
              }
       }

       if (version > 1) {
              if (version == 2 && m_sections.size() > 2)
                     cvwarn() << "CSegFrame: gor a version 2 segmentation, but more then two sections, this may be bogus\n";

              read_attribute_from_node(elm, "quality", m_quality, false);
              read_attribute_from_node(elm, "brightness", m_brightness, false);
              read_attribute_from_node(elm, "contrast", m_contrast, false);
       }
}

const std::string& CSegFrame::get_imagename() const
{
       return m_filename;
}

void CSegFrame::set_imagename(const std::string& name)
{
       m_filename = name;
}

void CSegFrame::rename_base(const std::string& new_base)
{
       bfs::path filename(m_filename);
       string suffix = filename.extension().string();
       string name = filename.stem().string();
       auto i = name.rbegin();
       int k = 0;

       while (i != name.rend() && isdigit(*i) ) {
              ++i;
              ++k;
       }

       string number(name.end() - k, name.end());
       m_filename = new_base + number + suffix;
}

const CSegFrame::Sections& CSegFrame::get_sections()const
{
       return m_sections;
}


const CSegStar& CSegFrame::get_star() const
{
       if (!m_has_star)
              cvwarn() << "CSegFrame::get_star(): returning fake star";

       return m_star;
}


void CSegFrame::write(CXMLElement& node, int version) const
{
       auto self = node.add_child("frame");
       self->set_attribute("image", m_filename);

       if (version > 1) {
              self->set_attribute("quality", to_string<float>(m_quality));
              self->set_attribute("brightness", to_string<float>(m_brightness));
              self->set_attribute("contrast", to_string<float>(m_contrast));
       }

       if (m_has_star)
              m_star.write(*self);

       for (auto i = m_sections.begin();
            i != m_sections.end(); ++i) {
              i->write(*self, version);
       }
}

const C2DBoundingBox CSegFrame::get_boundingbox() const
{
       C2DBoundingBox result;

       for (auto i = m_sections.begin();
            i != m_sections.end(); ++i) {
              result.unite(i->get_boundingbox());
       }

       return result;
}

void CSegFrame::shift(const C2DFVector& delta, const std::string& cropped_file)
{
       if (m_has_star)
              m_star.shift(delta);

       for (auto i = m_sections.begin();
            i != m_sections.end(); ++i)
              i->shift(delta);

       m_filename = cropped_file;
}

void CSegFrame::transform(const C2DTransformation& t)
{
       if (m_has_star)
              m_star.transform(t);

       for (auto i = m_sections.begin();
            i != m_sections.end(); ++i)
              i->transform(t);
}

void CSegFrame::set_image(P2DImage image)
{
       m_image = image;
}


void CSegFrame::inv_transform(const C2DTransformation& t)
{
       if (m_has_star)
              m_star.inv_transform(t);

       for (auto i = m_sections.begin(); i != m_sections.end(); ++i)
              i->inv_transform(t);
}


float CSegFrame::get_hausdorff_distance(const CSegFrame& other) const
{
       C2DPolygon p1;

       for (auto s = m_sections.begin();
            s != m_sections.end(); ++s)
              s->append_to(p1);

       C2DPolygon p2;

       for (auto s = other.m_sections.begin();
            s != other.m_sections.end(); ++s)
              s->append_to(p2);

       return p1.get_hausdorff_distance(p2);
}


C2DUBImage CSegFrame::get_section_masks(const C2DBounds& size) const
{
       C2DUBImage result(size);

       if (m_version == 2) {
              // in version 2 all sections are xor-ed,
              for (auto i = m_sections.begin();
                   i != m_sections.end(); ++i)
                     i->draw_xor(result);
       } else {
              unsigned char idx = 1;

              for (auto i = m_sections.begin();
                   i != m_sections.end(); ++i, ++idx)
                     i->draw(result, idx);
       }

       return result;
}

void CSegFrame::load_image() const
{
       m_image = load_image2d(m_filename);

       if (!m_image)
              throw create_exception<runtime_error>( "unable to find image file '", m_filename, "'");
}

C2DUBImage CSegFrame::get_section_masks() const
{
       if (!m_image)
              load_image();

       return get_section_masks(m_image->get_size());
}

C2DUBImage CSegFrame::get_section_masks(size_t n_sections) const
{
       if (!m_image)
              load_image();

       C2DUBImage result = get_section_masks(m_image->get_size());

       if (n_sections != 0 && n_sections != m_sections.size()) {
              const C2DFVector ray_a = m_star.m_directions[0];
              const double scale = n_sections / (2 * M_PI);
              // make a mask and re-run
              auto i = result.begin();

              for (size_t y = 0; y < result.get_size().y; ++y)  {
                     for (size_t x = 0; x < result.get_size().x; ++x, ++i)  {
                            if (*i) {
                                   const C2DFVector ray_b(static_cast<float>(x) - m_star.m_center.x,
                                                          static_cast<float>(y) - m_star.m_center.y);
                                   double a = scale * angle(ray_a, ray_b);

                                   if (a >= n_sections)
                                          a -= n_sections;

                                   *i = (unsigned char) (a + 1.0);
                            }
                     }
              }
       }

       return result;
}

struct EvalMaskStat: public TFilter<CSegFrame::SectionsStats> {

       EvalMaskStat(const C2DUBImage& mask):
              m_mask(mask)
       {
              max_idx = *max_element(mask.begin(), mask.end());
       };

       template <typename T>
       CSegFrame::SectionsStats operator ()(const T2DImage<T>& image) const
       {
              CSegFrame::SectionsStats result(max_idx);
              vector<size_t> size(max_idx, 0);
              auto k = image.begin();

              for (auto m = m_mask.begin(); m != m_mask.end(); ++k, ++m) {
                     const double v = *k;

                     if (*m) {
                            result[*m - 1].first  += v;
                            result[*m - 1].second += v * v;
                            ++size[*m - 1];
                     }
              }

              auto n = size.begin();

              for (auto i = result.begin(); i != result.end(); ++i, ++n) {
                     if (*n)
                            i->first /= *n;

                     if (*n > 1)
                            i->second = sqrt( (i->second - i->first * i->first * *n) / (*n - 1));
              }

              return result;
       }
private:
       const C2DUBImage& m_mask;
       unsigned char max_idx;
};

CSegFrame::SectionsStats CSegFrame::get_stats(const C2DUBImage& mask) const
{
       if (!m_image)
              load_image();

       if (mask.get_size() != m_image->get_size())
              throw create_exception<invalid_argument>( "Mask image ", mask.get_size(), " and data image ",
                            m_image->get_size(), " are of different size");

       return ::mia::filter(EvalMaskStat(mask), *m_image);
}

CSegFrame::SectionsStats CSegFrame::get_stats(size_t n_sections) const
{
       if (!m_image) {
              m_image = load_image2d(m_filename);

              if (!m_image)
                     throw create_exception<runtime_error>( "unable to find image file '", m_filename, "'");
       }

       C2DUBImage mask = get_section_masks(n_sections);
       return get_stats(mask);
}

size_t CSegFrame::get_nsections() const
{
       return m_sections.size();
}

void CSegFrame::set_quality(float q)
{
       m_quality = q;
}

void CSegFrame::set_brightness(float b)
{
       m_brightness = b;
}

void CSegFrame::set_contrast(float c)
{
       m_contrast = c;
}

float CSegFrame::get_quality() const
{
       return m_quality;
}

float CSegFrame::get_brightness() const
{
       return m_brightness;
}

float CSegFrame::get_contrast() const
{
       return m_contrast;
}


NS_MIA_END
