/*------------------------------------------------------------------------------
 src/lib/djImage/imageOpScale.cxx
 
 Copyright (c) 2004-2006 Darby Johnston.
 All rights reserved.
 Use is subject to license terms.
------------------------------------------------------------------------------*/

//#define DJ_DBG
#include "djImageOpXform.h"
#include <djMath.h>

namespace dj {

ImageOpScale::ImageOpScale()
{
  _size = Vec2i(500, 500);
  _filterX = AUTO;
  _filterY = AUTO;
  _edgeX = Image::EDGE_MIRROR;
  _edgeY = Image::EDGE_MIRROR;
}

const StrList ImageOpScale::attrNameList() const
  { return imageOpScaleAttrStr(); }

bool ImageOpScale::attr(const Str & in, const StrList & data)
{
  DJ_DBG_FNC("dj::ImageOpScale::attr");
  DJ_DBG_PRN("in = " << in);
  DJ_DBG_PRN("data = " << data);
  
  StrList tmp = data;
  if (compareCase(attrNameList()[ATTR_SIZE], in)) {
    if (! inflateStr(tmp, &_size)) return false;
  } else if (compareCase(attrNameList()[ATTR_FILTER], in)) {
    if (! inflateStr(tmp, &_filterX, imageOpScaleFilterStr())) return false;
    if (! inflateStr(tmp, &_filterY, imageOpScaleFilterStr())) return false;
  } else if (compareCase(attrNameList()[ATTR_EDGE], in)) {
    if (! inflateStr(tmp, &_edgeX, imageEdgeStr())) return false;
    if (! inflateStr(tmp, &_edgeY, imageEdgeStr())) return false;
  } else
    return false;
  return true;
}

const StrList ImageOpScale::attr(const Str & in) const
{
  StrList out;
  if (compareCase(attrNameList()[ATTR_SIZE], in))
    out += deflateStr(_size);
  if (compareCase(attrNameList()[ATTR_FILTER], in))
    out += deflateStr(_filterX, imageOpScaleFilterStr()) +
      deflateStr(_filterY, imageOpScaleFilterStr());
  if (compareCase(attrNameList()[ATTR_EDGE], in))
    out += deflateStr(_edgeX, imageEdgeStr()) +
      deflateStr(_edgeY, imageEdgeStr());
  return out;
}

const PixelDataInfo ImageOpScale::info(const InfoList & in) const
{
  return in.size() ? info(*in[0], _size) : PixelDataInfo();
}

const PixelDataInfo ImageOpScale::info(
  const PixelDataInfo & in, const Vec2i & size
) {
  return PixelDataInfo(size, in.pixel());
}

const Image ImageOpScale::work(const InputList & in) const
{
  return
    in.size() ?
    work(*in[0], _size, _filterX, _filterY, _edgeX, _edgeY) :
    Image();
}

// Filter functions.
//
typedef double (_FilterFnc)(const double t);

static const double _boxSupport = 0.5;
static double _boxFilter(double t)
{
  if (t > -0.5 && t <= 0.5) return 1.0;
  return 0.0;
}

static const double _triangleSupport = 1.0;
static double _triangleFilter(double t)
{
  if (t < 0.0) t = -t;
  if (t < 1.0) return 1.0 - t;
  return 0.0;
}

static const double _bellSupport = 1.5;
static double _bellFilter(double t)
{
  if (t < 0.0) t = -t;
  if (t < 0.5) return 0.75 - t * t;
  if (t < 1.5) {
    t = t - 1.5;
    return 0.5 * t * t;
  }
  return 0.0;
}

static const double _bsplineSupport = 2.0;
static double _bsplineFilter(double t)
{
  if (t < 0.0) t = -t;
  if (t < 1.0) {
    const double tt = t * t;
    return (0.5 * tt * t) - tt + 2.0 / 3.0;
  } else if (t < 2.0) {
    t = 2.0 - t;
    return (1.0 / 6.0) * (t * t * t);
  }
  return 0.0;
}

static double _sinc(double x)
{
  x *= pi;
  if (! isZero(x)) return sin(x) / x;
  return 1.0;
}
static const double _lanczos3Support = 3.0;
static double _lanczos3Filter(double t)
{
  if (t < 0.0) t = -t;
  if (t < 3.0) return _sinc(t) * _sinc(t / 3.0);
  return 0.0;
}

static const double _cubicSupport = 1.0;
static double _cubicFilter(double t)
{
  if (t < 0.0) t = -t;
  if (t < 1.0) return (2.0 * t - 3.0) * t * t + 1.0;
  return 0.0;
}

static const double _mitchellSupport = 2.0;
static double _mitchellFilter(double t)
{
  const double tt = t * t;
  static const double b = 1.0 / 3.0;
  static const double c = 1.0 / 3.0;
  if (t < 0.0) t = -t;
  if (t < 1.0) {
    t =
      ((12.0 - 9.0 * b - 6.0 * c) * (t * tt)) +
      ((-18.0 + 12.0 * b + 6.0 * c) * tt) +
      (6.0 - 2.0 * b);
    return t / 6.0;
  } else if (t < 2.0) {
    t =
      ((-1.0 * b - 6.0 * c) * (t * tt)) +
      ((6.0 * b + 30.0 * c) * tt) +
      ((-12.0 * b - 48.0 * c) * t) +
      (8.0 * b + 24.0 * c);
    return t / 6.0;
  }
  return 0.0;
}

static _FilterFnc * _filterFnc(ImageOpScale::FILTER in)
{
  static _FilterFnc * tmp [] = {
    0,
    _boxFilter,
    _triangleFilter,
    _bellFilter,
    _bsplineFilter,
    _lanczos3Filter,
    _cubicFilter,
    _mitchellFilter
  };
  return tmp[in];
}

static double _filterSupport(ImageOpScale::FILTER in)
{
  static const double tmp [] = {
    0.0,
    _boxSupport,
    _triangleSupport,
    _bellSupport,
    _bsplineSupport,
    _lanczos3Support,
    _cubicSupport,
    _mitchellSupport
  };
  return tmp[in];
}

// Pixel contributions.
//
static void _pixelContrib(
  int in, int out, int channels, ImageOpScale::FILTER filter, Image::EDGE edge,
  int ** pixels, double ** weights, int ** sizes
) {
  //DJ_DBG_FNC("dj::_pixelContrib");
  //DJ_DBG_PRN("in = " << in);
  //DJ_DBG_PRN("out = " << out);
  //DJ_DBG_PRN("edge = " << edge);

  // Filter function.
  //
  const ImageOpScale::FILTER f = ImageOpScale::AUTO == filter ?
    (in < out ? ImageOpScale::MITCHELL : ImageOpScale::LANCZOS3) :
    filter;
  //DJ_DBG_PRN("filter = " << imageOpScaleFilterStr()[f]);
  _FilterFnc * fnc = _filterFnc(f);
  const double support = _filterSupport(f);

  // XXX Edges?
  //
  const double scale = double(out) / double(in);
  //const double scale =
  //  (double(out > 1 ? (out - 1) : 1) / double(in > 1 ? (in - 1) : 1));

  const double s = (scale >= 1.0) ? 1.0 : (1.0 / scale);
  const double radius = support * s;
  //DJ_DBG_PRN("support = " << support);
  //DJ_DBG_PRN("scale = " << scale);
  //DJ_DBG_PRN("radius = " << radius);

  // Initialize.
  //
  *pixels = new int [out * ceil(radius * 2 + 1)];
  *weights = new double [out * ceil(radius * 2 + 1)];
  *sizes = new int [out];

  // Work.
  //
  int * pixelsP = *pixels;
  double * weightsP = *weights;
  int * sizeP = *sizes;
  int pixel = 0, pixelPrev = 0;
  for (int i = 0; i < out; ++i) {

    const double center = double(i) / scale;
    const int left = ceil(center - radius);
    const int right = floor(center + radius);
    //DJ_DBG_PRN("span = " << left << " " << center << " " << right);
    
    int count = 0;
    double weightSum = 0.0;
    for (int j = left; j <= right; ++j) {

      //pixel = clamp(j, 0, in);
      const int tmp = imageEdge(j, in, edge);
      if (-1 == tmp) continue;
      pixelPrev = pixel;
      pixel = tmp;
      
      const double weight = (scale < 1.0) ?
        ((*fnc)((center - double(j)) / s) / s) :
        (*fnc)(center - double(j));
      weightSum += weight;
      
      *pixelsP++ = (pixel - pixelPrev) * channels;
      *weightsP++ = weight;
      ++count;
      
      //DJ_DBG_PRN("pixel " << pixel << " (" << pixelPrev << ") = " << weight);
    }
    *sizeP++ = count;
    //DJ_DBG_PRN("weight sum = " << weightSum);

    // XXX Make sure weights sum to one?
    //
    if (! compare(weightSum, 1.0)) {
      const double scale = 1.0 / weightSum;
      weightsP -= count;
      for (int j = 0; j < count; ++j)
        *weightsP++ *= scale;
    }
  }
}

#define _ASSIGN_0(IN)
#define _ASSIGN_1(IN) \
  accumP[IN] = inP[IN] * weight

#define _ACCUM_0(IN)
#define _ACCUM_1(IN) \
  accumP[IN] += inP[IN] * weight

#define _WORK(A, B, C, D) \
  int size = *sizesP++; \
  if (size--) { \
    inP += *pixelsP++; \
    const double weight = *weightsP++; \
    _ASSIGN_##A(0); \
    _ASSIGN_##B(1); \
    _ASSIGN_##C(2); \
    _ASSIGN_##D(3); \
  } \
  while (size--) { \
    inP += *pixelsP++; \
    const double weight = *weightsP++; \
    _ACCUM_##A(0); \
    _ACCUM_##B(1); \
    _ACCUM_##C(2); \
    _ACCUM_##D(3); \
  }

#define _CONVERT_U8(IN) clamp(int(IN), pixelU8Min, pixelU8Max)
#define _CONVERT_U16(IN) clamp(int(IN), pixelU16Min, pixelU16Max)
#define _CONVERT_F32(IN) (IN)

#define _SCALE(TYPE) \
  \
  const Pixel##TYPE * inP = \
    reinterpret_cast<const Pixel##TYPE *>(in.data(0, x)); \
  Pixel##TYPE * outP = \
    reinterpret_cast<Pixel##TYPE *>(out->data(x, 0)); \
  int * pixelsP = pixels; \
  double * weightsP = weights; \
  int * sizesP = sizes; \
  \
  switch (channels) { \
  \
    case 1: \
      for (int y = 0; y < h; ++y, outP += inc) { \
        _WORK(1, 0, 0, 0) \
        outP[0] = _CONVERT_##TYPE(accumP[0]); \
      } \
      break; \
    case 2: \
      for (int y = 0; y < h; ++y, outP += inc) { \
        _WORK(1, 1, 0, 0) \
        outP[0] = _CONVERT_##TYPE(accumP[0]); \
        outP[1] = _CONVERT_##TYPE(accumP[1]); \
      } \
      break; \
    case 3: \
      for (int y = 0; y < h; ++y, outP += inc) { \
        _WORK(1, 1, 1, 0) \
        outP[0] = _CONVERT_##TYPE(accumP[0]); \
        outP[1] = _CONVERT_##TYPE(accumP[1]); \
        outP[2] = _CONVERT_##TYPE(accumP[2]); \
      } \
      break; \
    case 4: \
      for (int y = 0; y < h; ++y, outP += inc) { \
        _WORK(1, 1, 1, 1) \
        outP[0] = _CONVERT_##TYPE(accumP[0]); \
        outP[1] = _CONVERT_##TYPE(accumP[1]); \
        outP[2] = _CONVERT_##TYPE(accumP[2]); \
        outP[3] = _CONVERT_##TYPE(accumP[3]); \
      } \
      break; \
  \
    default: \
      for (int y = 0; y < h; ++y, outP += inc) { \
        for (int c = 0; c < channels; ++c) \
          accumP[c] = 0.0; \
        int size = *sizesP++; \
        while (size--) { \
          inP += *pixelsP++; \
          const double weight = *weightsP++; \
          for (int c = 0; c < channels; ++c) \
            accumP[c] += inP[c] * weight; \
        } \
        for (int c = 0; c < channels; ++c) \
          outP[c] = _CONVERT_##TYPE(accumP[c]); \
      } \
  }
  
static void _scale(
  const Image & in, Image * out,
  int inSize, int outSize, ImageOpScale::FILTER filter, Image::EDGE edge
) {

  // Pixel contributions.
  //
  const int channels = pixelSize(out->format());
  int * pixels = 0;
  double * weights = 0;
  int * sizes = 0;
  _pixelContrib(
    inSize, outSize, channels, filter, edge, &pixels, &weights, &sizes
  );
  
  // Work.
  //
  const int w = out->width(), h = out->height();
  const int inc = w * channels;
  Color accum(out->format(), PIXEL_F32);
  PixelF32 * accumP = reinterpret_cast<PixelF32 *>(accum.data());
  switch (out->type()) {
    case PIXEL_U8: for (int x = 0; x < w; ++x) { _SCALE(U8) } break;
    case PIXEL_U16: for (int x = 0; x < w; ++x) { _SCALE(U16) } break;
    case PIXEL_F32: for (int x = 0; x < w; ++x) { _SCALE(F32) } break;
  }
  
  if (pixels) delete [] pixels;
  if (weights) delete [] weights;
  if (sizes) delete [] sizes;
}
  
const Image ImageOpScale::work(
  const Image & in,
  const Vec2i & size,
  FILTER filterX, FILTER filterY,
  Image::EDGE edgeX, Image::EDGE edgeY
) {
  DJ_DBG_LOG();
  DJ_DBG_FNC("dj::ImageOpScale::work");
  DJ_DBG_PRN("input = " << in);
  DJ_DBG_PRN("size = " << size);
  DJ_DBG_PRN("filter = " << filterX << " " << filterY);
  DJ_DBG_PRN("edge = " << edgeX << " " << edgeY);
  
  // Setup.
  //
  Image out(size, in.pixel());
  out.tagList(in.tagList());
  if (! areaIsValid(size) || ! areaIsValid(in.size())) {
    out.zero();
    return out;
  }
  Image tmp(Vec2i(in.height(), out.width()), out.pixel());
  
  // Work
  //
  _scale(in, &tmp, in.width(), out.width(), filterX, edgeX);
  _scale(tmp, &out, in.height(), out.height(), filterY, edgeY);

  return out;
}

}
