#include <cmath>     // pow(-)
#include <stdexcept> /* std::domain_error(-) */
#include <limits>    // std::numeric_limits
#include <vector>
#include "igs_level_auto.h"
#include "igs_ifx_common.h" /* igs::image::rgba */

//------------------------------------------------------------
namespace {
double level_value_(double value, double mul_max, bool act_sw, double in_min,
                    double in_max, double out_min, double out_max,
                    double gamma) {
  if (act_sw) {
    if (in_max == in_min) {
      value = in_max;
    } else {
      /* 制限(in_min〜in_max) */
      if (in_min < in_max) {
        if (value < in_min) {
          value = in_min;
        } else if (in_max < value) {
          value = in_max;
        }
      } else {
        if (value < in_max) {
          value = in_max;
        } else if (in_min < value) {
          value = in_min;
        }
      }
      /* 正規化(0〜1) */
      value = (value - in_min) / (in_max - in_min);

      /* Gamma変換 */
      if ((1.0 != gamma) && (0.0 != gamma)) {
        value = pow(value, 1.0 / gamma);
      }
    }
    /* Outputの範囲にスケール変換 */
    value = out_min + value * (out_max - out_min);
    /* 0...1の間で制限かける */
    if (value < 0.0) {
      value = 0.0;
    } else if (1.0 < value) {
      value = 1.0;
    }
  }
  /* 0〜1.0 --> 0〜mul_maxスケール変換し、整数値化 */
  return floor(value * mul_max);
}
//------------------------------------------------------------
void level_ctable_template_(const unsigned int channels,
                            const bool *act_sw,          // user setting
                            const int *in_min,           // image pixel value
                            const int *in_max,           // image pixel value
                            const double *in_min_shift,  // user setting
                            const double *in_max_shift,  // user setting
                            const double *out_min,       // user setting
                            const double *out_max,       // user setting
                            const double *gamma,         // user setting
                            const unsigned int div_num,
                            std::vector<std::vector<unsigned int>> &table_array
                            /*
                            std::vector< std::vector<T> > &table_array
                     とするとvc2005mdで他プログラムとのリンク時、
                            allocatorの２重定義でエラーとなる。
                            std::vector< std::vector<unsigned int> >&table_array
                     ならOK
                     2009-01-27
                    */
                            ) {
  const double div_val = static_cast<double>(div_num);
  const double mul_val = div_val + 0.999999;
#if defined _WIN32  // vc compile_type
  double in_min_[4], in_max_[4];
#else
  double in_min_[channels], in_max_[channels];
#endif
  for (unsigned int cc = 0; cc < channels; ++cc) {
    in_min_[cc] = in_min_shift[cc] + in_min[cc] / div_val;
    in_max_[cc] = in_max_shift[cc] + in_max[cc] / div_val;
  }
  table_array.resize(channels);
  for (unsigned int cc = 0; cc < channels; ++cc) {
    table_array[cc].resize(div_num + 1);
    for (unsigned int yy = 0; yy <= div_num; ++yy) {
      table_array[cc][yy] = static_cast<unsigned int>(
          level_value_(yy / div_val, mul_val, act_sw[cc], in_min_[cc],
                       in_max_[cc], out_min[cc], out_max[cc], gamma[cc]));
    }
  }
}
//------------------------------------------------------------
template <class T>
void change_template_(T *image_array, const int height, const int width,
                      const int channels,

                      const bool *act_sw, const double *in_min_shift,
                      const double *in_max_shift, const double *out_min,
                      const double *out_max, const double *gamma) {
/* 1.まず最大値、最小値を求める */
#if defined _WIN32
  int in_min[4], in_max[4];
#else
  int in_min[channels], in_max[channels];
#endif
  for (int zz = 0; zz < channels; ++zz) {
    in_min[zz] = in_max[zz] = image_array[zz];
  }
  T *image_crnt = image_array;
  for (int yy = 0; yy < height; ++yy) {
    for (int xx = 0; xx < width; ++xx) {
      for (int zz = 0; zz < channels; ++zz) {
        if (image_crnt[zz] < in_min[zz]) {
          in_min[zz] = image_crnt[zz];
        } else if (in_max[zz] < image_crnt[zz]) {
          in_max[zz] = image_crnt[zz];
        }
      }
      image_crnt += channels;
    }
  }

  /* 2.最大値、最小値から変換テーブルを求める */
  std::vector<std::vector<unsigned int>> table_array;

  level_ctable_template_(channels, act_sw, in_min, in_max, in_min_shift,
                         in_max_shift, out_min, out_max, gamma,
                         std::numeric_limits<T>::max(), table_array);

  /* 3.変換テーブルを使ってlevel変換する */
  image_crnt        = image_array;
  const int pixsize = height * width;

  if (igs::image::rgba::siz == channels) {
    using namespace igs::image::rgba;
    for (int ii = 0; ii < pixsize; ++ii, image_crnt += channels) {
      image_crnt[red] = static_cast<T>(table_array[0][image_crnt[red]]);
      image_crnt[gre] = static_cast<T>(table_array[1][image_crnt[gre]]);
      image_crnt[blu] = static_cast<T>(table_array[2][image_crnt[blu]]);
      image_crnt[alp] = static_cast<T>(table_array[3][image_crnt[alp]]);
    }
  } else if (igs::image::rgb::siz == channels) {
    using namespace igs::image::rgb;
    for (int ii = 0; ii < pixsize; ++ii, image_crnt += channels) {
      image_crnt[red] = static_cast<T>(table_array[0][image_crnt[red]]);
      image_crnt[gre] = static_cast<T>(table_array[1][image_crnt[gre]]);
      image_crnt[blu] = static_cast<T>(table_array[2][image_crnt[blu]]);
    }
  } else if (1 == channels) { /* grayscale */
    for (int ii = 0; ii < pixsize; ++ii, ++image_crnt) {
      image_crnt[0] = static_cast<T>(table_array[0][image_crnt[0]]);
    }
  }

  table_array.clear();
}
}

void igs::level_auto::change(unsigned char *image_array,

                             const int height, const int width,
                             const int channels, const int bits,

                             const bool *act_sw,          // channels array
                             const double *in_min_shift,  // channels array
                             const double *in_max_shift,  // channels array
                             const double *out_min,       // channels array
                             const double *out_max,       // channels array
                             const double *gamma          // channels array
                             ) {
  if ((igs::image::rgba::siz != channels) &&
      (igs::image::rgb::siz != channels) && (1 != channels) /* grayscale */
      ) {
    throw std::domain_error("Bad channels,Not rgba/rgb/grayscale");
  }

  if (std::numeric_limits<unsigned char>::digits == bits) {
    change_template_(image_array, height, width, channels, act_sw, in_min_shift,
                     in_max_shift, out_min, out_max, gamma);
  } else if (std::numeric_limits<unsigned short>::digits == bits) {
    change_template_(reinterpret_cast<unsigned short *>(image_array), height,
                     width, channels, act_sw, in_min_shift, in_max_shift,
                     out_min, out_max, gamma);
  } else {
    throw std::domain_error("Bad bits,Not uchar/ushort");
  }
}
