/* -*- 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 <limits>
#include <sstream>
#include <mia/core/msgstream.hh>
#include <mia/core/scaler1d.hh>
#include <mia/core/utils.hh>
#include <mia/3d/filter/scale.hh>

#include <mia/core/threadedmsg.hh>
#include <mia/core/parallel.hh>

NS_BEGIN(scale_3dimage_filter)

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

template <class T>
C3DScale::result_type do_scale(const T3DImage<T>& src, const C3DBounds& target_size,
                               const C1DScalar& scaler_x,
                               const C1DScalar& scaler_y,
                               const C1DScalar& scaler_z)
{
       T3DImage<T> *result = new T3DImage<T>(target_size, src);
       cvdebug() << "do_scale: " << src.get_size() << " -> " << target_size << "\n";
// run x-scaling
       T3DImage<double> tmp(C3DBounds(target_size.x, src.get_size().y, src.get_size().z));
       auto filter_x = [&src, &tmp, &scaler_x](const C1DParallelRange & range) {
              vector<double> in_buffer(src.get_size().x);
              vector<double> out_buffer(tmp.get_size().x);

              for (auto z = range.begin(); z != range.end(); ++z)
                     for (size_t y = 0; y < src.get_size().y; ++y) {
                            copy(src.begin_at(0, y, z), src.begin_at(0, y + 1, z), in_buffer.begin());
                            scaler_x(in_buffer, out_buffer);
                            copy(out_buffer.begin(), out_buffer.end(), tmp.begin_at(0, y, z));
                     }
       };
       pfor(C1DParallelRange(0, src.get_size().z, 1), filter_x);
       // run y-scaling
       T3DImage<double> tmp2(C3DBounds(target_size.x, target_size.y, src.get_size().z));
       auto filter_y = [&tmp, &tmp2, &scaler_y](const C1DParallelRange & range) {
              vector<double> in_buffer(tmp.get_size().y);
              vector<double> out_buffer(tmp2.get_size().y);

              for (auto z = range.begin(); z != range.end(); ++z)
                     for (size_t x = 0; x < tmp.get_size().x; ++x) {
                            tmp.get_data_line_y(x, z, in_buffer);
                            scaler_y(in_buffer, out_buffer);
                            tmp2.put_data_line_y(x, z, out_buffer);
                     }
       };
       pfor(C1DParallelRange(0, tmp.get_size().z, 1), filter_y);
       auto filter_z = [&tmp2, result, &scaler_z](const C1DParallelRange & range) {
              vector<double> in_buffer(tmp2.get_size().z);
              vector<double> out_buffer(result->get_size().z);
              vector<T> out_buffer_T(result->get_size().z);

              for (auto y = range.begin(); y != range.end(); ++y)
                     for (size_t x = 0; x < tmp2.get_size().x; ++x) {
                            tmp2.get_data_line_z(x, y, in_buffer);
                            scaler_z(in_buffer, out_buffer);
                            transform(out_buffer.begin(), out_buffer.end(), out_buffer_T.begin(),
                            [](double x) {
                                   return mia_round_clamped<T>(x);
                            });
                            result->put_data_line_z(x, y, out_buffer_T);
                     }
       };
       pfor(C1DParallelRange(0, tmp2.get_size().y, 1), filter_z);
       return C3DScale::result_type(result);
}


template <class T>
C3DScale::result_type C3DScale::operator () (const T3DImage<T>& src) const
{
       TRACE_FUNCTION;
       C3DBounds target_size( m_size.x ? m_size.x : src.get_size().x,
                              m_size.y ? m_size.y : src.get_size().y,
                              m_size.z ? m_size.z : src.get_size().z );
       cvdebug() << "sizes: " << src.get_size() << " -> " << target_size << "\n";

       if (target_size == src.get_size())
              return C3DScale::result_type(new T3DImage<T>(src));

       C1DScalar scaler_x(*m_kernel, src.get_size().x, static_cast<size_t>(target_size.x));
       C1DScalar scaler_y(*m_kernel, src.get_size().y, static_cast<size_t>(target_size.y));
       C1DScalar scaler_z(*m_kernel, src.get_size().z, static_cast<size_t>(target_size.z));
       C3DFVector factor(float(src.get_size().x / float(target_size.x)),
                         float(src.get_size().y / float(target_size.y)),
                         float(src.get_size().z / float(target_size.z)));
       cvdebug() << "factor: " << factor << "\n";
       C3DScale::result_type result = do_scale(src, target_size,
                                               scaler_x, scaler_y, scaler_z);
       result->set_voxel_size(src.get_voxel_size() * factor);
       return C3DScale::result_type(result);
}


C3DScale::C3DScale(const C3DBounds& size, PSplineKernel kernel):
       m_size(size),
       m_kernel(kernel)
{
       TRACE_FUNCTION;
}

C3DScale::result_type C3DScale::do_filter(const C3DImage& image) const
{
       TRACE_FUNCTION;
       return mia::filter(*this, image);
}


C3DScaleFilterPlugin::C3DScaleFilterPlugin():
       C3DFilterPlugin("scale"),
       m_s(0, 0, 0)
{
       add_parameter("s", new C3DBoundsParameter(m_s, 0, "target size to set all components at once (component 0:use input image size)"));
       add_parameter("sx", make_nonnegative_param(m_s.x, false,
                     "target size in x direction (0:use input image size)"));
       add_parameter("sy", make_nonnegative_param(m_s.y, false,
                     "target size in y direction (0:use input image size)"));
       add_parameter("sz", make_nonnegative_param(m_s.z, false,
                     "target size in y direction (0:use input image size)"));
       add_parameter("interp", make_param(m_kernel, "bspline:d=3",  false, "interpolation kernel to be used "));
}


C3DFilter *C3DScaleFilterPlugin::do_create()const
{
       return new C3DScale(m_s, m_kernel);
}

const string C3DScaleFilterPlugin::do_get_descr()const
{
       return "3D image filter that scales to a given target size ";
}

C3DScaleFactor::C3DScaleFactor(const C3DFVector& factor, PSplineKernel kernel):
       m_factor(factor),
       m_kernel(kernel)
{
       TRACE_FUNCTION;
}



template <typename  T>
mia::C3DFilter::result_type C3DScaleFactor::operator () (const mia::T3DImage<T>& src) const
{
       TRACE_FUNCTION;
       C1DScalar scaler_x(*m_kernel, src.get_size().x, m_factor.x);
       C1DScalar scaler_y(*m_kernel, src.get_size().y, m_factor.y);
       C1DScalar scaler_z(*m_kernel, src.get_size().z, m_factor.z);
       C3DBounds target_size( scaler_x.get_output_size(),
                              scaler_y.get_output_size(),
                              scaler_z.get_output_size());

       if (target_size == src.get_size())
              return C3DScale::result_type(new T3DImage<T>(src));

       C3DScale::result_type result = do_scale(src, target_size,
                                               scaler_x, scaler_y, scaler_z);
       result->set_voxel_size(src.get_voxel_size() / m_factor);
       return C3DScale::result_type(result);
}


C3DScale::result_type C3DScaleFactor::do_filter(const mia::C3DImage& image) const
{
       return mia::filter(*this, image);
}

C3DScaleFactorFilterPlugin::C3DScaleFactorFilterPlugin():
       C3DFilterPlugin("fscale"),
       m_factor(1, 1, 1)
{
       add_parameter("fx", make_positive_param(m_factor.x, false, "scaling factor in x direction"));
       add_parameter("fy", make_positive_param(m_factor.y, false, "scaling factor in y direction"));
       add_parameter("fz", make_positive_param(m_factor.z, false, "scaling factor in z direction"));
       add_parameter("interp", make_param(m_kernel, "bspline:d=3",  false, "interpolation kernel to be used "));
}

mia::C3DFilter *C3DScaleFactorFilterPlugin::do_create()const
{
       return new C3DScaleFactor(m_factor, m_kernel);
}

const std::string C3DScaleFactorFilterPlugin::do_get_descr()const
{
       return "3D image filter that scales based on the given scale factor";
}




CIsoVoxel::CIsoVoxel(float voxelsize, PSplineKernel kernel):
       m_voxelsize(voxelsize),
       m_kernel(kernel)
{
}

CIsoVoxel::result_type CIsoVoxel::do_filter(const mia::C3DImage& image) const
{
       TRACE_FUNCTION;
       C3DFVector vs = image.get_voxel_size();
       C3DScaleFactor scaler(vs, m_kernel);
       return  scaler.filter(image);
}

CIsoVoxel::result_type CIsoVoxel::do_filter(P3DImage image) const
{
       TRACE_FUNCTION;
       C3DFVector vs = image->get_voxel_size();

       if (vs.x != m_voxelsize || vs.y != m_voxelsize || vs.z != m_voxelsize)
              return do_filter(*image);

       return image;
}



CIsoVoxelFilterPlugin::CIsoVoxelFilterPlugin():
       C3DFilterPlugin("isovoxel"),
       m_voxelsize(1.0)
{
       add_parameter("size", make_positive_param(m_voxelsize, false, "isometric target voxel size"));
       add_parameter("interp", make_param(m_kernel, "bspline:d=3",  false, "interpolation kernel to be used "));
}

mia::C3DFilter *CIsoVoxelFilterPlugin::do_create()const
{
       return new CIsoVoxel(m_voxelsize, m_kernel);
}

const std::string CIsoVoxelFilterPlugin::do_get_descr()const
{
       return "This filter scales an image to make the voxel size isometric "
              "and its size to correspond to the given value";
}

extern "C" EXPORT CPluginBase *get_plugin_interface()
{
       auto retval = new C3DScaleFilterPlugin();
       retval->append_interface(new CIsoVoxelFilterPlugin());
       return retval;
}

NS_END
